├── exception ├── yryd.py ├── __init__.py ├── klyd.py ├── mmkk.py └── common.py ├── schema ├── __init__.py ├── yryd.py ├── ddz.py ├── xyy.py ├── common.py ├── mmkk.py ├── ltwm.py ├── ymz.py └── klyd.py ├── script ├── __init__.py ├── v1 │ └── __init__.py ├── v2 │ ├── __init__.py │ ├── ddz_v2.py │ ├── ltwm_v2.py │ └── ymz_v2.py └── common │ └── __init__.py ├── app ├── api │ ├── __init__.py │ ├── callback │ │ ├── customize │ │ │ ├── ws.py │ │ │ └── __init__.py │ │ ├── __init__.py │ │ └── wx_bussiness │ │ │ ├── ierror.py │ │ │ ├── __init__.py │ │ │ ├── Sample.py │ │ │ └── WXBizMsgCrypt3.py │ └── sniff_data │ │ └── __init__.py └── __init__.py ├── config ├── ddz_example.yaml ├── ymz_example.yaml ├── ltwm_example.yaml ├── common_example.yaml ├── mmkk_example.yaml ├── xyy_example.yaml ├── yrrd_example.yaml ├── klyd_example.yaml ├── __init__.py └── biz_data.yaml ├── utils ├── __init__.py ├── global_utils.py ├── entry_utils.py ├── push_utils.py └── logger_utils.py ├── read_lt_info.py ├── read_xyy_info.py ├── read_yr_info.py ├── read_kl_info.py ├── read_mm_info.py ├── read_ymz_info.py ├── read_ymz.py ├── read_kl.py ├── read_lt.py ├── read_yr.py ├── read_mm.py ├── read_xyy.py ├── read_entry_url.py ├── README.md ├── read_fastapi.py └── LICENSE /exception/yryd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # yryd.py created by MoMingLog on 5/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-05 6 | 【功能描述】 7 | """ 8 | -------------------------------------------------------------------------------- /schema/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __init__.py.py created by MoMingLog on 28/3/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-03-28 6 | 【功能描述】 7 | """ 8 | -------------------------------------------------------------------------------- /script/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __init__.py.py created by MoMingLog on 28/3/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-03-28 6 | 【功能描述】 7 | """ 8 | -------------------------------------------------------------------------------- /app/api/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __init__.py.py created by MoMingLog on 19/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-19 6 | 【功能描述】 7 | """ 8 | -------------------------------------------------------------------------------- /app/api/callback/customize/ws.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ws.py created by MoMingLog on 19/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-19 6 | 【功能描述】 7 | """ 8 | -------------------------------------------------------------------------------- /exception/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __init__.py.py created by MoMingLog on 28/3/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-03-28 6 | 【功能描述】 7 | """ 8 | -------------------------------------------------------------------------------- /script/v1/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __init__.py.py created by MoMingLog on 1/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-01 6 | 【功能描述】 7 | """ 8 | -------------------------------------------------------------------------------- /script/v2/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __init__.py.py created by MoMingLog on 1/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-01 6 | 【功能描述】 7 | """ 8 | -------------------------------------------------------------------------------- /script/common/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __init__.py.py created by MoMingLog on 1/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-01 6 | 【功能描述】 7 | """ 8 | -------------------------------------------------------------------------------- /config/ddz_example.yaml: -------------------------------------------------------------------------------- 1 | account_data: 2 | # 下方内容完全可以配合我的抓包脚本一键上传 3 | 账号1: 4 | # 【选填】请求协议,如:http、https,不填则默认为http 5 | #protocol: "" 6 | # 【选填】请求主机地址,不填则默认为28917700289.sx.shuxiangby.cn 7 | #host: "" 8 | # 【必填】 9 | cookie: "" 10 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __init__.py.py created by MoMingLog on 28/3/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-03-28 6 | 【功能描述】 7 | """ 8 | from .entry_utils import EntryUrl 9 | from .global_utils import * 10 | from .logger_utils import Logger -------------------------------------------------------------------------------- /app/api/callback/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __init__.py.py created by MoMingLog on 19/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-19 6 | 【功能描述】 7 | """ 8 | from fastapi import APIRouter 9 | 10 | from .customize import customize_router 11 | 12 | callback_router = APIRouter() 13 | 14 | callback_router.include_router(customize_router, prefix="/ct", tags=["“回调”API"]) 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /read_lt_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # read_lt_info.py created by MoMingLog on 6/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-06 6 | 【功能描述】 7 | new Env("力天微盟阅读信息及提现"); 8 | 9 | 此任务只会打印力天微盟用户信息及其阅读情况,并且进行提现操作 10 | 11 | 配置【!!参考文件!!】在 config\ltwm_example.yaml中 12 | 13 | 提现相关配置在ltwm.yaml中,(第一次没有,请创建或将上方的参考文件重命名) 14 | """ 15 | 16 | from script.v2.ltwm_v2 import LTWMV2 17 | 18 | if __name__ == '__main__': 19 | LTWMV2(run_read_task=False) 20 | -------------------------------------------------------------------------------- /read_xyy_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # read_xyy_info.py created by MoMingLog on 13/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-13 6 | 【功能描述】 7 | new Env("小阅阅阅读信息及提现"); 8 | 9 | 此任务只会打印小阅阅用户信息及其阅读情况,并且进行提现操作 10 | 11 | 配置【!!参考文件!!】在 config\ xyy_example.yaml中 12 | 13 | 提现相关配置在xyy.yaml中,(第一次没有,请创建或将上方的参考文件重命名) 14 | 15 | """ 16 | 17 | from script.v2.xyy_v2 import XYYV2 18 | 19 | if __name__ == '__main__': 20 | XYYV2(run_read_task=False) 21 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __init__.py.py created by MoMingLog on 19/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-19 6 | 【功能描述】 7 | """ 8 | from fastapi import APIRouter 9 | 10 | from app.api.callback import callback_router 11 | from app.api.sniff_data import sniff_data_router 12 | 13 | all_router = APIRouter(prefix="/mmlg") 14 | 15 | all_router.include_router(callback_router, prefix="/callback") 16 | all_router.include_router(sniff_data_router, prefix="/sniff") 17 | -------------------------------------------------------------------------------- /exception/klyd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # klyd.py created by MoMingLog on 30/3/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-03-30 6 | 【功能描述】 7 | """ 8 | 9 | from exception.common import CommonException 10 | 11 | 12 | class FailedPassDetect(Exception): 13 | def __init__(self, message: str = "检测未通过,此账号停止运行!"): 14 | super().__init__(f"{message}") 15 | 16 | 17 | class WithdrawFailed(CommonException): 18 | def __init__(self, msg: str): 19 | super().__init__(f"提现失败, {msg}", "🟡💰") 20 | -------------------------------------------------------------------------------- /read_yr_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # read_yr_info.py created by MoMingLog on 6/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-06 6 | 【功能描述】 7 | new Env("鱼儿阅读信息及提现"); 8 | 9 | 此任务只会打印鱼儿用户信息及其阅读情况,并且进行提现操作 10 | 11 | 统一入口链接:http://h5.pyqvr23agj8.cn/pipa_read?upuid=2068422 12 | 13 | 如果进不去,可以先运行一下 “read_entry_url.py”,如果青龙任务添加成功,应该称为 “阅读入口” 14 | 15 | 配置【!!参考文件!!】在 config\yryd_example.yaml中 16 | 17 | 提现相关配置在yryd.yaml中,(第一次没有,请创建或将上方的参考文件重命名) 18 | """ 19 | from script.v2.yryd_v2 import YRYDV2 20 | 21 | if __name__ == '__main__': 22 | YRYDV2(run_read_task=False) -------------------------------------------------------------------------------- /read_kl_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # read_kl_info.py created by MoMingLog on 6/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-06 6 | 【功能描述】 7 | new Env("可乐阅读信息及提现"); 8 | 9 | 此任务只会打印可乐用户信息及其阅读情况,并且进行提现操作 10 | 11 | 统一入口链接:http://kl04061517.ue8vlnl7tb.cn/?upuid=1316875 12 | 13 | 如果进不去,可以先运行一下 “read_entry_url.py”,如果青龙任务添加成功,应该称为 “阅读入口” 14 | 15 | 配置【!!参考文件!!】在 config\klyd_example.yaml中 16 | 17 | 提现相关配置在klyd.yaml中,(第一次没有,请创建或将上方的参考文件重命名) 18 | """ 19 | 20 | from script.v2.klyd_v2 import KLYDV2 21 | 22 | if __name__ == '__main__': 23 | KLYDV2(run_read_task=False) 24 | -------------------------------------------------------------------------------- /config/ymz_example.yaml: -------------------------------------------------------------------------------- 1 | # 【选填】提现密码(全局配置) 2 | # 功能描述:这里是全局配置,生效范围是所有账号(会被单独配置覆盖,其余未单独配置的则使用全局配置) 3 | pwd: 6666 4 | 5 | # 【选填】是否自动睡眠等待下一批文章的到来(全局配置,但会被单独配置覆盖,其余未单独配置的则使用全局配置) 6 | wait_next_read: true 7 | 8 | # 【选填】是否提现(全局配置,但会被单独配置覆盖,其余未单独配置的则使用全局配置) 9 | is_withdraw: true 10 | 11 | # 【选填】设置提现金额(全局配置,但会被单独配置覆盖,其余未单独配置的则使用全局配置) 12 | # 不填,则会根据已有的提现选项,从大到小匹配 13 | #withdraw: 3 14 | 15 | account_data: 16 | # 这是用户名,可以自行更改 17 | 账号1: 18 | # 【必填】主页面就有,无需抓包 19 | userShowId: 123456 20 | # 【必填】最好是手机号码后四位(当前是单独配置,会覆盖全局配置) 21 | pwd: 6666 22 | # 【选填】是否自动睡眠等待下一批文章的到来 23 | wait_next_read: true -------------------------------------------------------------------------------- /read_mm_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # read_mm_info.py created by MoMingLog on 6/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-06 6 | 【功能描述】 7 | 8 | new Env("猫猫阅读信息及提现"); 9 | 10 | 此任务只会打印猫猫用户信息及其阅读情况,并且进行提现操作 11 | 12 | 统一入口链接:http://9pw4.dsdtew.shop/haobaobao/auth/f5097609e2ff70f696af4c1ed8b3ed4e 13 | 14 | 如果进不去,可以先运行一下 “read_entry_url.py”,如果青龙任务添加成功,应该称为 “阅读入口” 15 | 16 | 配置【!!参考文件!!】在 config\mmkk_example.yaml中 17 | 18 | 提现相关配置在mmkk.yaml中,(第一次没有,请创建或将上方的参考文件重命名) 19 | """ 20 | 21 | from script.v2.mmkk_v2 import MMKKV2 22 | 23 | if __name__ == '__main__': 24 | MMKKV2(run_read_task=False) 25 | -------------------------------------------------------------------------------- /read_ymz_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # read_ymz_info.py created by MoMingLog on 18/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-18 6 | 【功能描述】 7 | new Env("有米赚"); 8 | 0 30 7,10,13 * * * read_ymz.py 9 | 10 | 统一入口链接: http://i3n0nzg2wcvnhzu6opsu.xoa8m3pb4.zhijianzzmm.cn/ttz/wechat/ttzScanCode?userShowId=5332 11 | 12 | 请确认完整拉库,此脚本的配置不依赖环境变量(为了后序账号数据添加、修改、删除方便一点) 13 | 14 | 配置【!!参考文件!!】在 config\ ymz_example.yaml中,请先阅读其中的注释内容,把【必填】都填上就可以运行了 15 | 16 | 实际生效的【配置文件名】为:ymz.yaml(第一次没有,请创建或将上方的参考文件重命名) 17 | 18 | 参考文件每次拉库都会拉取,注意里面可能会添加新的内容 19 | 20 | 如果需要其他额外的配置,请详细的阅读对应的注释内容并配置 21 | """ 22 | 23 | from script.v2.ymz_v2 import YMZV2 24 | 25 | if __name__ == '__main__': 26 | YMZV2() 27 | -------------------------------------------------------------------------------- /read_ymz.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # read_ymz.py created by MoMingLog on 18/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-18 6 | 【功能描述】 7 | new Env("有米赚用户信息及提现"); 8 | 0 30 8,15,20 * * * read_ymz.py 9 | 10 | 此任务只会打印有米赚的用户信息,并且进行提现操作 11 | 12 | 统一入口链接: http://i3n0nzg2wcvnhzu6opsu.xoa8m3pb4.zhijianzzmm.cn/ttz/wechat/ttzScanCode?userShowId=5332 13 | 14 | 请确认完整拉库,此脚本的配置不依赖环境变量(为了后序账号数据添加、修改、删除方便一点) 15 | 16 | 配置【!!参考文件!!】在 config\ ymz_example.yaml中,请先阅读其中的注释内容,把【必填】都填上就可以运行了 17 | 18 | 实际生效的【配置文件名】为:ymz.yaml(第一次没有,请创建或将上方的参考文件重命名) 19 | 20 | 参考文件每次拉库都会拉取,注意里面可能会添加新的内容 21 | 22 | 如果需要其他额外的配置,请详细的阅读对应的注释内容并配置 23 | """ 24 | 25 | from script.v2.ymz_v2 import YMZV2 26 | 27 | if __name__ == '__main__': 28 | YMZV2(run_read_task=False) 29 | -------------------------------------------------------------------------------- /read_kl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # read_kl.py created by MoMingLog on 2/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-02 6 | 【功能描述】 7 | new Env("可乐读书"); 8 | 0 0 7-23 * * * read_kl.py 9 | 10 | 统一入口链接:http://kl04061517.ue8vlnl7tb.cn/?upuid=1316875 11 | 12 | 如果进不去,可以先运行一下 “read_entry_url.py”,如果青龙任务添加成功,应该称为 “阅读入口” 13 | 14 | 请确认完整拉库,此脚本的配置不依赖环境变量(为了后序账号cookie添加、修改、删除方便一点) 15 | 16 | 配置【!!参考文件!!】在 config\klyd_example.yaml中,请先阅读其中的注释内容,把【必填】都填上就可以运行了 17 | 18 | 实际生效的【配置文件名】为:klyd.yaml(第一次没有,请创建或将上方的参考文件重命名) 19 | 20 | 参考文件每次拉库都会拉取,注意里面可能会添加新的内容(我也会尽量在已有的klyd.yaml自动化添加) 21 | 22 | 如果需要其他额外的配置,请详细的阅读对应的注释内容并配置 23 | 24 | !!!不熟练的先拿小号测试!!! 25 | """ 26 | from script.v2.klyd_v2 import KLYDV2 27 | 28 | if __name__ == '__main__': 29 | KLYDV2() 30 | -------------------------------------------------------------------------------- /read_lt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # read_lt.py created by MoMingLog on 5/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-05 6 | 【功能描述】 7 | 8 | new Env("力天微盟"); 9 | 0 0 7-23 * * * read_lt.py 10 | 11 | 统一入口链接:http://e9adf325c38844188a2f0aefaabb5e0d.op20skd.toptomo.cn/?fid=12286 12 | 13 | 或者可以在TG通知群里扫我的邀请二维码犒劳一下作者 14 | 15 | 请确认完整拉库,此脚本的配置不依赖环境变量(为了后序账号cookie添加、修改、删除方便一点) 16 | 17 | 配置【!!参考文件!!】在 config\ltwm_example.yaml中,请先阅读其中的注释内容,把【必填】都填上就可以运行了 18 | 19 | 实际生效的【配置文件名】为:ltwm.yaml(第一次没有,请创建或将上方的参考文件重命名) 20 | 21 | 参考文件每次拉库都会拉取,注意里面可能会添加新的内容(我也会尽量在已有的ltwm.yaml自动化添加) 22 | 23 | 如果需要其他额外的配置,请详细的阅读对应的注释内容并配置 24 | 25 | !!!不熟练的先拿小号测试!!! 26 | 27 | """ 28 | 29 | from script.v2.ltwm_v2 import LTWMV2 30 | 31 | if __name__ == '__main__': 32 | LTWMV2() 33 | -------------------------------------------------------------------------------- /read_yr.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # read_yr.py created by MoMingLog on 6/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-06 6 | 【功能描述】 7 | new Env("鱼儿阅读"); 8 | 0 45 7-23 * * * read_yr.py 9 | 10 | 统一入口链接:http://h5.pyqvr23agj8.cn/pipa_read?upuid=2068422 11 | 12 | 如果进不去,可以先运行一下 “read_entry_url.py”,如果青龙任务添加成功,应该称为 “阅读入口” 13 | 14 | 请确认完整拉库,此脚本的配置不依赖环境变量(为了后序账号cookie添加、修改、删除方便一点) 15 | 16 | 配置【!!参考文件!!】在 config\yryd_example.yaml中,请先阅读其中的注释内容,把【必填】都填上就可以运行了 17 | 18 | 实际生效的【配置文件名】为:yryd.yaml(第一次没有,请创建或将上方的参考文件重命名) 19 | 20 | 参考文件每次拉库都会拉取,注意里面可能会添加新的内容(我也会尽量在已有的yryd.yaml自动化添加) 21 | 22 | 如果需要其他额外的配置,请详细的阅读对应的注释内容并配置 23 | 24 | !!!不熟练的先拿小号测试!!! 25 | """ 26 | 27 | from script.v2.yryd_v2 import YRYDV2 28 | 29 | if __name__ == '__main__': 30 | YRYDV2() 31 | -------------------------------------------------------------------------------- /read_mm.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # read_mm.py created by MoMingLog on 3/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-03 6 | 【功能描述】 7 | 8 | new Env("猫猫看看"); 9 | 0 30 7-23 * * * read_mm.py 10 | 11 | 统一入口链接:http://9pw4.dsdtew.shop/haobaobao/auth/f5097609e2ff70f696af4c1ed8b3ed4e 12 | 13 | 如果进不去,可以先运行一下 “read_entry_url.py”,如果青龙任务添加成功,应该称为 “阅读入口” 14 | 15 | 请确认完整拉库,此脚本的配置不依赖环境变量(为了后序账号cookie添加、修改、删除方便一点) 16 | 17 | 配置【!!参考文件!!】在 config\mmkk_example.yaml中,请先阅读其中的注释内容,把【必填】都填上就可以运行了 18 | 19 | 实际生效的【配置文件名】为:mmkk.yaml(第一次没有,请创建或将上方的参考文件重命名) 20 | 21 | 参考文件每次拉库都会拉取,注意里面可能会添加新的内容(我也会尽量在已有的mmkk.yaml自动化添加) 22 | 23 | 如果需要其他额外的配置,请详细的阅读对应的注释内容并配置 24 | 25 | !!!不熟练的先拿小号测试!!! 26 | """ 27 | from script.v2.mmkk_v2 import MMKKV2 28 | 29 | if __name__ == '__main__': 30 | MMKKV2() 31 | -------------------------------------------------------------------------------- /config/ltwm_example.yaml: -------------------------------------------------------------------------------- 1 | # 【注意】 2 | # 1. “#”符号表示注释,被注释掉的配置是【不会生效】,如果想让其生效,请去掉它 3 | # 2. 请确保格式正确,否则会报错(只需要删除一个符号即可,其他的空格不用删除) 4 | # 3. 实在不理解的可以前往 https://t.me/+A6PlLqiqK19mNzNl 问一问其他已了解的伙伴 5 | # 4. 标注了“可单独配置”的,皆可以在对应的账号下添加,优先级更高 6 | 7 | # 【选填】 全局延迟配置(可单独配置,直接在对应的账号中添加相同的格式即可) 8 | # 功能描述:完成阅读,获取阅读文章前的睡眠时间 9 | delay: 10 | # 模拟阅读时间,默认随机 11-16 11 | # Tips: 最小不能低于8秒(网络传输有延迟,最好高一点,也可以按照默认的不改动) 12 | read_delay: 13 | - 11 14 | - 16 15 | 16 | 17 | # 【选填】是否需要提现,默认为 true,表示需要(可单独配置) 18 | # 功能描述:可以用于阅读信息获取任务,限制提现请求 19 | is_withdraw: true 20 | 21 | # 【选填】 最大线程数,默认为 1 22 | # 功能描述:设置线程池的最大线程数,每个线程可以单独跑一个账号 23 | #max_thread_count: 1 24 | 25 | # 【必填】 26 | account_data: 27 | # 账号名(自定义) 28 | 账号1: 29 | #【必填】 30 | authorization: "auth_1" 31 | 32 | # 账号2: 33 | # authorization: "auth_2" 34 | # 账号3: 35 | # authorization: "auth_3" -------------------------------------------------------------------------------- /read_xyy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # read_xyy.py created by MoMingLog on 13/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-13 6 | 【功能描述】 7 | new Env("小阅阅"); 8 | 0 30 7-23 * * * read_xyy.py 9 | 10 | 统一入口链接:http://04130809.v0xxg.shop/yunonline/v1/auth/5729acffb4b05596aef08e18eaf8a7cd?codeurl=04130809.v0xxg.shop&codeuserid=2&time=1711531311 11 | 12 | 如果进不去,可以先运行一下 “read_entry_url.py”,如果青龙任务添加成功,应该称为 “阅读入口” 13 | 14 | 请确认完整拉库,此脚本的配置不依赖环境变量(为了后序账号cookie添加、修改、删除方便一点) 15 | 16 | 配置【!!参考文件!!】在 config\ xyy_example.yaml中,请先阅读其中的注释内容,把【必填】都填上就可以运行了 17 | 18 | 实际生效的【配置文件名】为:xyy.yaml(第一次没有,请创建或将上方的参考文件重命名) 19 | 20 | 参考文件每次拉库都会拉取,注意里面可能会添加新的内容(我也会尽量在已有的xyy.yaml自动化添加) 21 | 22 | 如果需要其他额外的配置,请详细的阅读对应的注释内容并配置 23 | 24 | !!!不熟练的先拿小号测试!!! 25 | """ 26 | 27 | from script.v2.xyy_v2 import XYYV2 28 | 29 | if __name__ == '__main__': 30 | XYYV2() 31 | -------------------------------------------------------------------------------- /app/api/callback/wx_bussiness/ierror.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ######################################################################### 4 | # Author: jonyqin 5 | # Created Time: Thu 11 Sep 2014 01:53:58 PM CST 6 | # File Name: ierror.py 7 | # Description:定义错误码含义 8 | ######################################################################### 9 | WXBizMsgCrypt_OK = 0 10 | WXBizMsgCrypt_ValidateSignature_Error = -40001 11 | WXBizMsgCrypt_ParseXml_Error = -40002 12 | WXBizMsgCrypt_ComputeSignature_Error = -40003 13 | WXBizMsgCrypt_IllegalAesKey = -40004 14 | WXBizMsgCrypt_ValidateCorpid_Error = -40005 15 | WXBizMsgCrypt_EncryptAES_Error = -40006 16 | WXBizMsgCrypt_DecryptAES_Error = -40007 17 | WXBizMsgCrypt_IllegalBuffer = -40008 18 | WXBizMsgCrypt_EncodeBase64_Error = -40009 19 | WXBizMsgCrypt_DecodeBase64_Error = -40010 20 | WXBizMsgCrypt_GenReturnXml_Error = -40011 21 | -------------------------------------------------------------------------------- /read_entry_url.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # read_entry_url.py created by MoMingLog on 3/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-03 6 | 【功能描述】 7 | new Env('阅读入口'); 8 | 0 0 6 * * * read_entry_url.py 9 | 10 | 用来获取所有的入口链接,目前已适配的平台如下: 11 | - 小阅阅 12 | - 猫猫看看 13 | - 可乐读书 14 | - 鱼儿阅读 15 | """ 16 | 17 | from utils.entry_utils import EntryUrl 18 | 19 | if __name__ == '__main__': 20 | all_entry_url = EntryUrl.get_all_entry_url(is_flag=True) 21 | msg_list = [] 22 | for entry_name, entry_url in all_entry_url.items(): 23 | if isinstance(entry_url, list): 24 | msg_list.append(f"【{entry_name}】") 25 | for url in entry_url: 26 | if isinstance(url, tuple): 27 | msg_list.append(f"> {url[0]} >> {url[1]}") 28 | else: 29 | msg_list.append(f"> {url}") 30 | else: 31 | msg_list.append(f"【{entry_name}】\n> {entry_url}") 32 | 33 | try: 34 | # 采用青龙面板拉库成功时自动添加的notify.py文件中的方法 35 | from notify import send 36 | 37 | send("\n".join(msg_list)) 38 | except Exception as e: 39 | print("\n".join(msg_list)) 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 该项目仅供参考学习,切勿用于其他不合法用途! 2 | 3 | 欢迎大家针对项目中代码、逻辑等的不足、可优化以及bug,向作者提出建议 4 | 5 | # 简单文档记录 6 | 7 | 🥤 于 2024.04.03 运行正常 8 | 9 | 😸 于 2024.04.03 运行正常 10 | 11 | [TG通知](https://t.me/mmlg_ql) 12 | 13 | > [!TIP] 14 | > 下方是作者使用的环境 15 | > 16 | > 青龙面板版本:`2.17.2`(whyour/qinglong:debian) 17 | > 18 | > 开发环境Python版本: `3.10` 19 | 20 | ### 2.17.2版本青龙拉库命令-v2 21 | 22 | ```shell 23 | ql repo https://github.com/MoMingRose/WXRead.git "read" "" ".*" "" "py|yaml" 24 | ``` 25 | 26 | ### 2.16.2版本 27 | 28 | ```shell 29 | ql repo https://github.com/MoMingRose/WXRead.git "read" "" ".*" "master" "py|yaml" 30 | ``` 31 | ### 2.15.4版本 32 | 33 | ```shell 34 | ql repo https://github.com/MoMingRose/WXRead.git "read" "" "config|exception|schema|script|utils" "master" "py|yaml" 35 | ``` 36 | 37 | ### python3依赖 38 | 39 | - 必装依赖 40 | 41 | ```text 42 | httpx 43 | pydantic==1.10.12 44 | colorama 45 | pyyaml 46 | ``` 47 | 48 | - 选装依赖 49 | 50 | ```text 51 | ujson 52 | ``` 53 | 54 | 55 | ### 配置环境 56 | 57 | 个人觉得针对这个项目来说 如果配置env 则会比较杂乱,不容易修改 58 | 59 | 所以此项目采用`yaml`文件进行配置,具体可参考对应任务的`example.yaml`文件 60 | 61 | 在项目下的`config`文件夹中,里面有具体的注释 62 | 63 | 如果这个项目让你感到心情愉悦,可以支持一下,点个Start 64 | 65 | 土豪大佬也可以请作者吃个鸡腿 -------------------------------------------------------------------------------- /schema/yryd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # yryd.py created by MoMingLog on 3/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-03 6 | 【功能描述】 7 | """ 8 | from typing import Type, Dict 9 | 10 | from pydantic import BaseModel, Field, create_model, HttpUrl 11 | 12 | from schema.common import CommonPartConfig, CommonGlobalConfig 13 | 14 | 15 | class CommonYRYDConfig(BaseModel): 16 | withdraw_type: str = Field(None, description="提现类型: wx 微信, ali 支付宝") 17 | 18 | 19 | class YRYDAccount(CommonPartConfig, CommonYRYDConfig): 20 | """鱼儿阅读(局部配置)""" 21 | cookie: str | None = Field(None, description="用户cookie") 22 | 23 | 24 | class BaseYRYDGlobalConfig(CommonGlobalConfig, CommonYRYDConfig): 25 | """鱼儿阅读(全局配置)""" 26 | biz_data: list | None = Field(None, description="检测文章的biz") 27 | 28 | 29 | YRYDConfig: Type[BaseYRYDGlobalConfig] = create_model( 30 | 'YRYDConfig', 31 | account_data=(Dict[str | int, YRYDAccount], {}), 32 | source=(str, "yryd.yaml"), 33 | __base__=BaseYRYDGlobalConfig 34 | ) 35 | 36 | 37 | class RspReadUrl(BaseModel): 38 | """获取阅读链接的响应""" 39 | jump: str | None = Field(None, description="阅读链接") 40 | 41 | 42 | class RspDoRead(BaseModel): 43 | """do_read响应""" 44 | error_msg: str | None = Field(None, description="错误信息") 45 | jkey: str | None = Field(None, description="阅读key") 46 | url: str | HttpUrl | None = Field(None, description="阅读链接") 47 | -------------------------------------------------------------------------------- /app/api/callback/wx_bussiness/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __init__.py.py created by MoMingLog on 19/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-19 6 | 【功能描述】 7 | """ 8 | from fastapi import APIRouter 9 | from starlette.requests import Request 10 | from starlette.responses import Response 11 | 12 | from app.callback.wx_bussiness.WXBizMsgCrypt3 import WXBizMsgCrypt 13 | 14 | sToken = "hJqcu3uJ9Tn2gXPmxx2w9kkCkCE2EPYo" 15 | sEncodingAESKey = "6qkdMrq68nTKduznJYO1A37W2oEgpkMUvkttRToqhUt" 16 | sCorpID = "ww1436e0e65a779aee" 17 | 18 | router = APIRouter(prefix="/wxchat") 19 | 20 | 21 | @router.get("/wxchat") 22 | async def wxBizMsgDetected(msg_signature, timestamp, nonce, echostr): 23 | wxcpt = WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID) 24 | ret, sEchoStr = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr) 25 | if ret != 0: 26 | print(f"错误: VerifyURL 返回值: {ret}") 27 | else: 28 | print("URL验证成功, 回声字符串:", sEchoStr) 29 | return Response(sEchoStr) 30 | 31 | 32 | @router.post("/wxchat") 33 | async def wxBizMsgDetected(msg_signature, timestamp, nonce, request: Request): 34 | wxcpt = WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID) 35 | body = await request.body() 36 | ret, sMsg = wxcpt.DecryptMsg(body, msg_signature, timestamp, nonce) 37 | if ret != 0: 38 | print(f"错误: DecryptMsg 返回值: {ret}") 39 | else: 40 | print("消息解密成功, 消息内容:", sMsg) 41 | -------------------------------------------------------------------------------- /schema/ddz.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ddz.py created by MoMingLog on 17/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-17 6 | 【功能描述】 7 | """ 8 | from typing import Type, Dict 9 | 10 | from pydantic import create_model, BaseModel, Field 11 | 12 | from schema.common import CommonPartConfig, CommonGlobalConfig 13 | from utils import extract_urls 14 | 15 | 16 | class CommonDDZConfig(BaseModel): 17 | """点点赚全局和局部的相同配置""" 18 | withdraw_type: str = Field(None, description="提现类型: wx 微信, ali 支付宝") 19 | protocol: str | None = Field(None, description="协议") 20 | 21 | 22 | class DDZAccount(CommonPartConfig, CommonDDZConfig): 23 | """点点赚(局部配置)""" 24 | host: str 25 | cookie: str 26 | 27 | 28 | class BaseDDZGlobalConfig(CommonGlobalConfig, CommonDDZConfig): 29 | """点点赚(全局配置)""" 30 | pass 31 | 32 | 33 | DDZConfig: Type[BaseDDZGlobalConfig] = create_model( 34 | "DDZConfig", 35 | account_data=(Dict[str | int, DDZAccount], {}), 36 | source=(str, "ddz.yaml"), 37 | __base__=BaseDDZGlobalConfig 38 | ) 39 | 40 | 41 | class CommonRsp(BaseModel): 42 | code: int 43 | msg: str 44 | 45 | 46 | class RspQrCode(CommonRsp): 47 | data: str | None = Field(None, description="二维码路径") 48 | web_url: str | None = Field(None, description="包含跳转链接和相关说明") 49 | 50 | def jump_url(self): 51 | if self.web_url: 52 | return extract_urls(self.web_url) 53 | 54 | def __str__(self): 55 | jump_url = self.jump_url() 56 | if jump_url is None: 57 | jump_url = "未提取成功跳转链接!" 58 | return "\n".join([ 59 | "二维码信息", 60 | f"❄️>> 二维码路径: {self.data}", 61 | f"❄️>> 跳转链接: {jump_url}", 62 | f"❄️>> 官方描述: {self.web_url.replace(jump_url, '').strip()}" 63 | ]) 64 | 65 | def __repr__(self): 66 | return self.__str__() 67 | -------------------------------------------------------------------------------- /exception/mmkk.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # mmkk.py created by MoMingLog on 28/3/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-03-28 6 | 【功能描述】 7 | """ 8 | 9 | 10 | class ReadValid(Exception): 11 | """当前阅读暂时无效,请稍候再来""" 12 | 13 | def __init__(self, msg): 14 | super().__init__(f"获取文章阅读链接失败,原因:{msg}") 15 | 16 | 17 | class FailedFetch(Exception): 18 | """提取失败""" 19 | 20 | def __init__(self, pre_str: str, message: str = "请通知作者更新脚本"): 21 | super().__init__(f"{pre_str}, {message}") 22 | 23 | 24 | class FailedFetchUK(FailedFetch): 25 | """提取UK失败""" 26 | 27 | # 设置默认的错误消息 28 | def __init__(self): 29 | super().__init__("提取用户UK失败") 30 | 31 | 32 | class FailedFetchArticleJSUrl(FailedFetch): 33 | def __init__(self): 34 | super().__init__("提取article.js链接失败") 35 | 36 | 37 | class FailedFetchArticleJSVersion(FailedFetch): 38 | def __init__(self): 39 | super().__init__("提取article.js版本失败") 40 | 41 | 42 | class FailedFetchReadUrl(FailedFetch): 43 | def __init__(self, e): 44 | super().__init__(f"提取阅读链接失败, 原因: {e}") 45 | 46 | 47 | class ArticleJSUpdated(Exception): 48 | """article.js已更新""" 49 | 50 | def __init__(self, latest_version: str): 51 | super().__init__(f"官方接口更新至【v{latest_version}】版本,请通知作者更新脚本!") 52 | 53 | 54 | class CodeChanged(Exception): 55 | """代码更新""" 56 | 57 | def __init__(self): 58 | super().__init__("官方代码发生变化,请通知作者更新脚本!") 59 | 60 | 61 | class PauseReading(Exception): 62 | def __init__(self, message): 63 | super().__init__(f"暂停阅读: {message}") 64 | 65 | 66 | class ReachedLimit(Exception): 67 | pass 68 | 69 | 70 | class StopRun(Exception): 71 | def __init__(self, message): 72 | super().__init__(f"🈲 {message},脚本停止运行, 请快去通知小伙伴们!") 73 | 74 | 75 | class WithDrawFailed(Exception): 76 | pass 77 | 78 | 79 | class StopRunWithShowMsg(Exception): 80 | def __init__(self, message): 81 | super().__init__(f"🈲 {message}") 82 | -------------------------------------------------------------------------------- /exception/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # common.py created by MoMingLog on 2/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-02 6 | 【功能描述】 7 | """ 8 | import re 9 | 10 | 11 | class CommonException(Exception): 12 | def __init__(self, msg: str, graphics: str): 13 | super().__init__(f"{graphics} {msg}") 14 | 15 | 16 | class PauseReadingAndCheckWait(CommonException): 17 | def __init__(self, msg: str): 18 | super().__init__(f"暂停阅读, {msg}", "🟢🔶") 19 | 20 | 21 | class PauseReadingTurnNext(CommonException): 22 | def __init__(self, msg: str, graphics: str = None): 23 | if graphics is None: 24 | graphics = "🟡" 25 | else: 26 | graphics = "🟢🔶" 27 | super().__init__(f"暂停阅读, {msg}", graphics) 28 | 29 | 30 | class StopReadingNotExit(CommonException): 31 | def __init__(self, msg: str): 32 | super().__init__(f"停止阅读, {msg}", "🟡") 33 | 34 | 35 | class StopReadingAndExit(CommonException): 36 | def __init__(self, msg: str, graphics: str = "🔴"): 37 | super().__init__(f"停止阅读, {msg}", graphics) 38 | 39 | 40 | class CookieExpired(CommonException): 41 | def __init__(self): 42 | super().__init__("Cookie已过期,请更新!", "🔴") 43 | 44 | 45 | class RspAPIChanged(CommonException): 46 | def __init__(self, api: str): 47 | super().__init__(f"{api} 接口返回数据变化,请更新!", "🔴") 48 | 49 | 50 | class APIChanged(CommonException): 51 | def __init__(self, api: str): 52 | super().__init__(f"{api} 接口变化,请更新!", "🔴") 53 | 54 | 55 | class ExitWithCodeChange(CommonException): 56 | def __init__(self, prefix=""): 57 | super().__init__(f"{prefix} 官方貌似更新了源代码,脚本已停止运行!", "🔴") 58 | 59 | 60 | class Exit(CommonException): 61 | def __init__(self, msg=None): 62 | s = f", 原因: {msg}" if msg is not None else "" 63 | super().__init__(f"出现不可挽回异常{s}, 退出脚本", "🔴") 64 | 65 | 66 | class FailedPushTooManyTimes(CommonException): 67 | def __init__(self): 68 | super().__init__("超过最大推送失败次数,请配置好相关数据!", "🔴") 69 | 70 | 71 | class NoSuchArticle(Exception): 72 | def __init__(self, msg): 73 | super().__init__(msg) 74 | 75 | 76 | class RegExpError(Exception): 77 | def __init__(self, reg: str | re.Pattern): 78 | if isinstance(reg, re.Pattern): 79 | reg = reg.pattern.__str__() 80 | super().__init__(f"提取失败! 请通知作者更新下方正则\n> {reg}") 81 | -------------------------------------------------------------------------------- /app/api/callback/wx_bussiness/Sample.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | ######################################################################### 4 | # 作者: jonyqin 5 | # 创建时间: Thu 11 Sep 2014 03:55:41 PM CST 6 | # 文件名: Sample.py 7 | # 描述: WXBizMsgCrypt 使用示例文件 8 | ######################################################################### 9 | from WXBizMsgCrypt3 import WXBizMsgCrypt 10 | import xml.etree.ElementTree as ET 11 | import sys 12 | 13 | if __name__ == "__main__": 14 | # 假设企业在企业微信后台设置的参数如下 15 | sToken = "hJqcu3uJ9Tn2gXPmxx2w9kkCkCE2EPYo" 16 | sEncodingAESKey = "6qkdMrq68nTKduznJYO1A37W2oEgpkMUvkttRToqhUt" 17 | sCorpID = "ww1436e0e65a779aee" 18 | 19 | # 使用示例一:验证回调URL 20 | # 当企业开启回调模式时,企业号会向验证URL发送一个GET请求 21 | wxcpt = WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID) 22 | sVerifyMsgSig = "012bc692d0a58dd4b10f8dfe5c4ac00ae211ebeb" 23 | sVerifyTimeStamp = "1476416373" 24 | sVerifyNonce = "47744683" 25 | sVerifyEchoStr = "fsi1xnbH4yQh0+PJxcOdhhK6TDXkjMyhEPA7xB2TGz6b+g7xyAbEkRxN/3cNXW9qdqjnoVzEtpbhnFyq6SVHyA==" 26 | ret, sEchoStr = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr) 27 | if ret != 0: 28 | print(f"错误: VerifyURL 返回值: {ret}") 29 | sys.exit(1) 30 | print("URL验证成功, 回声字符串:", sEchoStr) 31 | 32 | # 使用示例二:解密用户的回复 33 | sReqMsgSig = "0c3914025cb4b4d68103f6bfc8db550f79dcf48e" 34 | sReqTimeStamp = "1476422779" 35 | sReqNonce = "1597212914" 36 | sReqData = """ 37 | 38 | 39 | """ 40 | ret, sMsg = wxcpt.DecryptMsg(sReqData, sReqMsgSig, sReqTimeStamp, sReqNonce) 41 | if ret != 0: 42 | print(f"错误: DecryptMsg 返回值: {ret}") 43 | sys.exit(1) 44 | print("解密成功, 消息内容:", sMsg) 45 | 46 | # 使用示例三:加密回复用户的消息 47 | sRespData = """ww1436e0e65a779aeeChenJiaShun 48 | 1476422779text你好1456453720 49 | 1000002""" 50 | ret, sEncryptMsg = wxcpt.EncryptMsg(sRespData, sReqNonce, sReqTimeStamp) 51 | if ret != 0: 52 | print(f"错误: EncryptMsg 返回值: {ret}") 53 | sys.exit(1) 54 | print("加密成功, 加密消息:", sEncryptMsg) 55 | -------------------------------------------------------------------------------- /config/common_example.yaml: -------------------------------------------------------------------------------- 1 | # 所有的配置信息都可以在schema\common.py中找到 2 | # 这里的配置可应用到所有任务的所有账号中 3 | # 默认配置策略是任务优先;大白话就是:如果任务配置和common配置相同,优先使用任务配置 4 | # 每个任务相同的配置可以移动到这里来,那么就可以把任务配置中的配置项注释掉 5 | 6 | # 【选填】控制台是否打印调试内容,不清楚的不要开,否则控制台会非常的杂乱 7 | # 默认关闭 8 | debug: false 9 | 10 | # 【选填】任务配置策略,任务优先还是common优先,1:common 2:任务 11 | # 功能描述:common 表示common.yaml配置文件;任务表示对应的阅读配置文件,例如 klyd.yaml 12 | # 默认为 2,任务优先 13 | strategy: 2 14 | 15 | # 【选填】是否初始化 colorama 配置,默认初始化 16 | # 功能描述:开关颜色渲染,true: 关闭渲染, false: 开启渲染 17 | # Tips: 当此项为True青龙面板的颜色渲染会消失,这样或许可以避免不支持颜色显示的青龙面板出现乱码的现象 18 | # Tips: 目前不知道什么原理,经过测试可以关掉颜色渲染,比较方便,哈哈 19 | init_colorama: true 20 | 21 | # 【选填】阅读随机睡眠时间 22 | # 默认随机阅读睡眠时间10-20秒,默认等待推送检测睡眠时间19(暂不随机) 23 | delay: 24 | read_delay: [ 10, 20 ] 25 | push_delay: [ 19 ] 26 | 27 | # 【选填】推送方式,1: WxPusher 2: 企业微信(可单独配置) 28 | # 功能描述:选择对应的推送平台,推送检测文章信息(可多选) 29 | # Tips: 默认推送方式为 WxPusher 30 | # 方法 1: 31 | #push_types: [1, 2, 3] 32 | # 方法 2: 33 | #push_types: 34 | # - 1 35 | # - 2 36 | # - 3 37 | 38 | # --------------WxPusher配置------------------- 39 | # 【选填】统一WxPusher的appToken, 默认为空,并注释(启用请删掉前面的“#”) 40 | # 【!!注意!!】此配置会受上方的任务配置策略影响, 41 | # 如果为2,请确认对应的任务中的appToken已被注释掉(行首加“#”) 42 | #appToken: "" 43 | 44 | # 【选填】WxPusher中的主题ID(可单独配置),下面两种方式选择一个即可 45 | # 配置方式 1:字符串 (此方式只能设置一个主题) 46 | #topicIds: "123456" 47 | # 配置方式2:数组 (此方式可以设置多个主题) 48 | #topicIds: ["123456", "789123"] 49 | # 或者下方数组格式 50 | #topicIds: 51 | # - "123456" 52 | # - "789123" 53 | 54 | # --------------WxPusher配置------------------- 55 | 56 | # --------------企业微信配置------------------- 57 | # 【选填】 是否使用 企业微信机器人推送(可单独配置) 58 | # 功能描述:当 push_type为 2 时生效,默认为True 59 | #use_robot: true 60 | # 【必/选填】 企业微信机器人的webhook地址(可单独配置) 61 | # 功能描述:当 use_robot为 true 时生效 62 | #webhook_url: "https://..." 63 | 64 | # 【必/选填】企业ID 65 | # 功能描述:当 push_types==2 且 use_robot==true 时生效,此时此项必填 66 | #corp_id: 123456 67 | # 【必/选填】应用密钥 68 | # 功能描述: 当 push_types==2 且 use_robot==true 时生效,此时此项必填 69 | #corp_secret: "123456" 70 | # 【必/选填】应用ID 71 | # 功能描述: 当 push_types==2 且 use_robot==true 时生效,此时此项必填 72 | #agent_id: 123456 73 | # --------------企业微信配置------------------- 74 | 75 | # --------------检测文章访问回调配置------------------- 76 | # 此检测可以通过两种方式实现 77 | # 方式一:本阅读程序,也就是不用安装部署其它,只需要按照下方配置项的提示来操作即可 78 | # 方式二:单独部署对应的API程序,支持Docker一键部署,对应的github仓库地址如下: 79 | # https://github.com/MoMingRose/WxReadDetect 80 | 81 | # 【选填】是否使用websocket来完成检测文章的访问确认“回调”操作 82 | # 功能描述:可以用来检测,用户是否访问了推送的检测文章链接 83 | is_use_ws: true 84 | # 【选/必填】websocket的域名及端口 85 | # 功能描述:如果开启了“回调”操作,则此项必填!!! 86 | # Tips: 过检测机子如果在外网,可以使用内网穿透青龙面板所在地址来完成回调操作 87 | # Tips: 本地机子过检测,则把localhost改成你访问青龙面板的局域网地址 88 | # Tips: !!! 注意青龙的docker配置中一定要映射16699端口,确保能够访问,才能进行后续的操作 89 | # 公益API地址: mmlg.back1.hpnu.cn 或者 back1.hpnu.cn:16699 90 | ws_host: "mmlg.back1.hpnu.cn" 91 | # --------------检测文章访问回调配置------------------- 92 | 93 | # 【选填】 最大检测未通过的账号数量 94 | # 功能描述:当达到这个数量时,整个程序将会自动退出,默认为 0表示不作处理 95 | max_failed_pass_count: 0 96 | 97 | # 【选填】是否启用“打印响应”功能,该功能主要用于调试,为避免生成较大的日志文件,故设置了此开关 98 | # 【功能描述】打印网络的响应情况:响应状态码、发起请求者(账号名)、请求链接、请求头、请求体(如果有)、响应头、响应体(如果有) 99 | is_log_response: false 100 | 101 | -------------------------------------------------------------------------------- /utils/global_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # global_utils.py created by MoMingLog on 28/3/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-03-28 6 | 【功能描述】 7 | """ 8 | import asyncio 9 | import hashlib 10 | import re 11 | import time 12 | from datetime import datetime 13 | 14 | 15 | def timestamp(length=10): 16 | """ 17 | 获取时间戳(13位) 18 | :param length: 长度,默认为13 19 | :return: 13位时间戳 20 | """ 21 | return int(time.time() * 10 ** (length - 10)) 22 | 23 | 24 | def md5(content): 25 | m = hashlib.md5() 26 | m.update(content.encode("utf-8")) 27 | return m.hexdigest() 28 | 29 | 30 | def get_date(is_fill_chinese=False): 31 | if is_fill_chinese: 32 | return time.strftime("%Y年%m月%d日 %H时%M分%S秒", time.localtime()) 33 | return time.strftime("%Y-%m-%d", time.localtime()) 34 | 35 | 36 | def is_date_after_today(date_str): 37 | """判断传进来的时间是否比今天要靠后""" 38 | try: 39 | parsed_date = datetime.strptime(date_str, "%Y-%m-%d").date() 40 | today = datetime.today().date() 41 | return parsed_date > today 42 | except ValueError: 43 | return False 44 | 45 | 46 | def hide_dynamic_middle(s, visible_ratio=0.7, mask_char='*'): 47 | n = len(s) 48 | # 根据可见比例计算可见字符的总数量 49 | visible_count = int(n * visible_ratio) 50 | 51 | # 确保至少显示一些字符 52 | if visible_count < 2: 53 | visible_count = 2 54 | 55 | # 计算每边显示的字符数 56 | show_each_side = visible_count // 2 57 | 58 | # 如果可见字符数量是奇数,增加末尾的显示数量 59 | if visible_count % 2 != 0: 60 | show_end = show_each_side + 1 61 | else: 62 | show_end = show_each_side 63 | 64 | show_start = show_each_side 65 | 66 | # 构建隐藏后的字符串 67 | if n > visible_count: 68 | return s[:show_start] + (mask_char * (n - visible_count)) + s[-show_end:] 69 | else: 70 | return s 71 | 72 | 73 | def extract_urls(text): 74 | # 定义一个正则表达式模式来匹配大部分URL 75 | url_pattern = r'https?://[\w\-\.]+[\w\-]+[\w\-\./\?\=\&\%]*' 76 | # 使用findall方法查找所有匹配的URL 77 | urls = re.findall(url_pattern, text) 78 | if len(urls) == 1: 79 | return urls[0] 80 | else: 81 | return urls 82 | 83 | 84 | def run_async(async_func, *args, **kwargs): 85 | """ 86 | 一个通用函数,用于在同步代码中运行异步函数。 87 | 88 | :param async_func: 异步函数 89 | :param args: 传递给异步函数的位置参数 90 | :param kwargs: 传递给异步函数的关键字参数 91 | :return: 异步函数的返回值 92 | """ 93 | # 尝试获取当前线程的事件循环,如果没有则创建新的事件循环 94 | try: 95 | loop = asyncio.get_event_loop() 96 | if loop.is_closed(): 97 | raise RuntimeError("Event loop is closed") 98 | except RuntimeError as e: 99 | loop = asyncio.new_event_loop() 100 | asyncio.set_event_loop(loop) 101 | result = None 102 | # 在事件循环中运行异步任务 103 | try: 104 | result = loop.run_until_complete(async_func(*args, **kwargs)) 105 | # 如果是新创建的事件循环,运行完毕后应该关闭 106 | if loop != asyncio.get_running_loop(): 107 | loop.close() 108 | except RuntimeError: 109 | pass 110 | 111 | return result 112 | 113 | 114 | if __name__ == '__main__': 115 | print(md5("/index/mob/get_zan_qr.html")) 116 | -------------------------------------------------------------------------------- /config/mmkk_example.yaml: -------------------------------------------------------------------------------- 1 | # 【注意】 2 | # 1. “#”符号表示注释,被注释掉的配置是【不会生效】,如果想让其生效,请去掉它 3 | # 2. 请确保格式正确,否则会报错(只需要删除一个符号即可,其他的空格不用删除) 4 | # 3. 实在不理解的可以前往 https://t.me/+A6PlLqiqK19mNzNl 问一问其他已了解的伙伴 5 | # 4. 标注了“可单独配置”的,皆可以在对应的账号下添加,优先级更高 6 | 7 | # 【选填】推送方式,1: WxPusher 2: 企业微信(可单独配置) 8 | # 功能描述:选择对应的推送平台,推送检测文章信息(可多选) 9 | # Tips: 默认推送方式为 WxPusher 10 | # 方法 1: 11 | #push_types: [1, 2, 3] 12 | # 方法 2: 13 | #push_types: 14 | # - 1 15 | # - 2 16 | # - 3 17 | 18 | # --------------WxPusher配置------------------- 19 | # 【必/选填】 WxPusher推送通知的appToken(可单独配置) 20 | # 功能描述:当出现检测文章链接时,自动推送到对应账号 21 | # Tips: 可以在云桌面搭建微信机器人,用来自动监听此消息,从而完成自动过检测功能 22 | # Tips: 可在common.yaml为所有任务配置统一的appToken,此时这里的appToken可以省略 23 | # Tips: 由于默认【配置策略】为【任务优先】,也就是此文件配置优先 24 | # 故,如果这里是空字符串,则会覆盖common.yaml中的【配置策略】 25 | #appToken: "" 26 | 27 | # 【选填】WxPusher中的主题ID(可单独配置),下面两种方式选择一个即可 28 | # 配置方式 1:字符串 (此方式只能设置一个主题) 29 | #topicIds: "123456" 30 | # 配置方式2:数组 (此方式可以设置多个主题) 31 | #topicIds: ["123456", "789123"] 32 | # 或者下方数组格式 33 | #topicIds: 34 | # - "123456" 35 | # - "789123" 36 | # --------------WxPusher配置------------------- 37 | 38 | # --------------企业微信配置------------------- 39 | # 【选填】 是否使用 企业微信机器人推送(可单独配置) 40 | # 功能描述:当 push_type为 2 时生效,默认为True 41 | #use_robot: true 42 | # 【必/选填】 企业微信机器人的webhook地址(可单独配置) 43 | # 功能描述:当 use_robot为 true 时生效 44 | #webhook_url: "https://..." 45 | 46 | # 【必/选填】企业ID 47 | # 功能描述:当 push_types==2 且 use_robot==true 时生效,此时此项必填 48 | #corp_id: "123456" 49 | # 【必/选填】应用密钥 50 | # 功能描述: 当 push_types==2 且 use_robot==true 时生效,此时此项必填 51 | #corp_secret: "123456" 52 | # 【必/选填】应用ID 53 | # 功能描述: 当 push_types==2 且 use_robot==true 时生效,此时此项必填 54 | #agent_id: 123456 55 | # --------------企业微信配置------------------- 56 | 57 | # 【选填】 全局延迟配置(可单独配置,直接在对应的账号中添加相同的格式即可) 58 | # 功能描述:完成阅读,获取阅读文章前的睡眠时间 59 | #delay: 60 | # # 模拟阅读时间,默认10-20 61 | # # Tips: 最小不能低于6秒 62 | # read_delay: 63 | # - 10 64 | # - 20 65 | # # 推送检测文章后的等待时间 66 | # push_delay: 67 | # - 19 68 | 69 | # 【目前暂不支持】【选填】自动等待下批文章阅读(可单独配置),默认为 false 70 | # 功能描述:出现如:"下一批文章将在42分钟后到来"的消息后,是否自动睡眠 71 | #wait_next_read: true 72 | # 【目前暂不支持】【选填】 最大线程数,默认为 1 73 | # 功能描述:设置线程池的最大线程数,每个线程可以单独跑一个账号 74 | #max_thread_count: 1 75 | 76 | # 【选填】 全局的支付宝提现账号(可单独配置),默认为空,则表示微信提现 77 | #aliName: "" 78 | #aliAccount: "" 79 | 80 | # 【选填】是否需要提现,默认为 true,表示需要(可单独配置) 81 | # 功能描述:可以用于阅读信息获取任务,限制提现请求 82 | is_withdraw: true 83 | 84 | # 【选填】全局提现金额(单位:元)(可单独配置) 85 | #withdraw: 0.9 86 | 87 | # 【选填】第一次循环是否走推送 88 | # 功能描述:不管当前具体是第几轮第几篇,只要是刚运行程序,那么第一篇文章是否走推送 89 | first_while_to_push: False 90 | 91 | # 【必填】 92 | account_data: 93 | # 账号名(自定义) 94 | 账号1: 95 | # 【必填】 96 | # 抓包简述:开启抓包后,进入主页面,再回到抓包软件,筛选出链接中包含 user or workinfo,复制其中的cookie值即可 97 | cookie: "klyd_1" 98 | # 【必填】WxPusher中的用户UID,检测文章通知会单独发给这个微信用户 99 | uid: "1" 100 | 101 | # 账号2: 102 | # cookie: "klyd_2" 103 | # uid: "2" 104 | # 账号3: 105 | # cookie: "klyd_3" 106 | # uid: "3" 107 | # 用于漏网之鱼检测(数据来源:幻生大佬) 108 | biz_data: 109 | - "MzkxNTE3MzQ4MQ==" 110 | - "Mzg5MjM0MDEwNw==" 111 | - "MzUzODY4NzE2OQ==" 112 | - "MzkyMjE3MzYxMg==" 113 | - "MzkxNjMwNDIzOA==" 114 | - "Mzg3NzUxMjc5Mg==" 115 | - "Mzg4NTcwODE1NA==" 116 | - "Mzk0ODIxODE4OQ==" 117 | - "Mzg2NjUyMjI1NA==" 118 | - "MzIzMDczODg4Mw==" 119 | - "Mzg5ODUyMzYzMQ==" 120 | - "MzU0NzI5Mjc4OQ==" 121 | - "Mzg5MDgxODAzMg==" 122 | - "MzIzMDczODg4Mw==" 123 | - "MzkxNDU1NDEzNw==" -------------------------------------------------------------------------------- /app/api/callback/customize/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __init__.py.py created by MoMingLog on 19/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-19 6 | 【功能描述】 7 | """ 8 | 9 | from typing import Dict 10 | 11 | from fastapi import APIRouter, WebSocket, WebSocketDisconnect 12 | from starlette.requests import Request 13 | from starlette.responses import HTMLResponse 14 | 15 | customize_router = APIRouter() 16 | 17 | 18 | class ConnectionManager: 19 | def __init__(self): 20 | self.active_connections: Dict[str, WebSocket] = {} 21 | 22 | async def connect(self, websocket: WebSocket, client_id: str): 23 | await websocket.accept() 24 | self.active_connections[client_id] = websocket 25 | 26 | def disconnect(self, client_id: str): 27 | try: 28 | del self.active_connections[client_id] 29 | except KeyError: 30 | pass 31 | 32 | async def send_personal_message(self, message: str, client_id: str): 33 | websocket = self.active_connections.get(client_id) 34 | if websocket: 35 | await websocket.send_text(message) 36 | 37 | async def broadcast(self, message: str): 38 | for websocket in self.active_connections.values(): 39 | await websocket.send_text(message) 40 | 41 | 42 | manager = ConnectionManager() 43 | 44 | 45 | @customize_router.websocket("/ws/{client_id}") 46 | async def websocket_endpoint(websocket: WebSocket, client_id: str): 47 | # 连接客户端 48 | await manager.connect(websocket, client_id) 49 | try: 50 | while True: 51 | data = await websocket.receive_text() 52 | # 假设数据格式为 "target_id:message" 53 | target_id, message = data.split(":") 54 | await manager.send_personal_message(message, target_id) 55 | except WebSocketDisconnect: 56 | manager.disconnect(client_id) 57 | await manager.broadcast(f"Client #{client_id} has left.") 58 | 59 | 60 | @customize_router.get("/get-link", response_class=HTMLResponse) 61 | def get_link(redirect: str, client_id: str, target_id: str, request: Request): 62 | domain = request.base_url.hostname # 获取当前请求的主机名或 IP 地址 63 | port = request.base_url.port # 获取当前请求的端口 64 | if port is not None: 65 | domain = f"{domain}:{port}" 66 | wx_url = f"ws://{domain}/mmlg/callback/ct/ws/{client_id}" 67 | 68 | return f""" 69 | 70 | 71 | 72 | 自动回调并跳转 73 | 74 | 75 |

client_id: {client_id}

76 |

target_id: {target_id}

77 |

78 | 94 | 95 | 96 | """ 97 | -------------------------------------------------------------------------------- /config/xyy_example.yaml: -------------------------------------------------------------------------------- 1 | # 可参考其它参考配置文件,基本都是通用的 2 | # 能自定义ua,就自定义吧 3 | 4 | # 【注意】 5 | # 1. “#”符号表示注释,被注释掉的配置是【不会生效】,如果想让其生效,请去掉它 6 | # 2. 请确保格式正确,否则会报错(只需要删除一个符号即可,其他的空格不用删除) 7 | # 3. 实在不理解的可以前往 https://t.me/+A6PlLqiqK19mNzNl 问一问其他已了解的伙伴 8 | # 4. 标注了“可单独配置”的,皆可以在对应的账号下添加,优先级更高 9 | 10 | # 【选填】推送方式,1: WxPusher 2: 企业微信(可单独配置) 11 | # 功能描述:选择对应的推送平台,推送检测文章信息(可多选) 12 | # Tips: 默认推送方式为 WxPusher 13 | # 方法 1: 14 | #push_types: [1, 2, 3] 15 | # 方法 2: 16 | #push_types: 17 | # - 1 18 | # - 2 19 | # - 3 20 | 21 | # --------------WxPusher配置------------------- 22 | # 【必/选填】 WxPusher推送通知的appToken(可单独配置) 23 | # 功能描述:当出现检测文章链接时,自动推送到对应账号 24 | # Tips: 可以在云桌面搭建微信机器人,用来自动监听此消息,从而完成自动过检测功能 25 | # Tips: 可在common.yaml为所有任务配置统一的appToken,此时这里的appToken可以省略 26 | # Tips: 由于默认【配置策略】为【任务优先】,也就是此文件配置优先 27 | # 故,如果这里是空字符串,则会覆盖common.yaml中的【配置策略】 28 | #appToken: "" 29 | 30 | # 【选填】WxPusher中的主题ID(可单独配置),下面两种方式选择一个即可 31 | # 配置方式 1:字符串 (此方式只能设置一个主题) 32 | #topicIds: "123456" 33 | # 配置方式2:数组 (此方式可以设置多个主题) 34 | #topicIds: ["123456", "789123"] 35 | # 或者下方数组格式 36 | #topicIds: 37 | # - "123456" 38 | # - "789123" 39 | # --------------WxPusher配置------------------- 40 | 41 | # --------------企业微信配置------------------- 42 | # 【选填】 是否使用 企业微信机器人推送(可单独配置) 43 | # 功能描述:当 push_type为 2 时生效,默认为True 44 | #use_robot: true 45 | # 【选填】 是否使用 markdown 格式文本推送,默认不使用,仅推送文章链接 46 | # 功能描述:markdown格式更好看,但是有的微信可能不支持(群友反馈) 47 | #is_push_markdown: false 48 | # 【必/选填】 企业微信机器人的webhook地址(可单独配置) 49 | # 功能描述:当 use_robot为 true 时生效 50 | #webhook_url: "https://..." 51 | 52 | # 【必/选填】企业ID 53 | # 功能描述:当 push_types==2 且 use_robot==true 时生效,此时此项必填 54 | #corp_id: "123456" 55 | # 【必/选填】应用密钥 56 | # 功能描述: 当 push_types==2 且 use_robot==true 时生效,此时此项必填 57 | #corp_secret: "123456" 58 | # 【必/选填】应用ID 59 | # 功能描述: 当 push_types==2 且 use_robot==true 时生效,此时此项必填 60 | #agent_id: 123456 61 | # --------------企业微信配置------------------- 62 | 63 | # 【选填】 全局延迟配置(可单独配置,直接在对应的账号中添加相同的格式即可) 64 | # 功能描述:完成阅读,获取阅读文章前的睡眠时间 65 | #delay: 66 | # # 模拟阅读时间,默认10-20 67 | # # Tips: 最小不能低于6秒 68 | # read_delay: 69 | # - 10 70 | # - 20 71 | # # 推送检测文章后的等待时间 72 | # push_delay: 73 | # - 19 74 | 75 | # 【选填】自动等待下批文章阅读(可单独配置),默认为 false 76 | # 功能描述:出现如:"下一批文章将在42分钟后到来"的消息后,是否自动睡眠 77 | #wait_next_read: true 78 | # 【选填】 最大线程数,默认为 1 79 | # 功能描述:设置线程池的最大线程数,每个线程可以单独跑一个账号 80 | #max_thread_count: 1 81 | 82 | # 【选填】是否需要提现,默认为 true,表示需要 (可单独配置) 83 | # 功能描述:可以用于阅读信息获取任务,限制提现请求 84 | is_withdraw: true 85 | 86 | # 【选填】提现额度限制(可单独配置) 87 | # 功能描述:只有达到或者大于这个金额才允许提现 88 | #withdraw: 0.9 89 | 90 | # 【选填】提现方式:wx or ali (可单独配置) 91 | # 功能描述:wx表示微信提现,ali表示支付宝提现 92 | #withdraw_type: wx 93 | # 【选填】 全局的支付宝提现账号(可单独配置),默认为 None 94 | # 功能描述:先在上方选择 ali,再在此处填写对应的支付宝账号 95 | # Tips: 如果这里不填写,则会使用默认已经绑定的支付宝(如果账号已经绑定过,这里就算填写其他的,也会使用账号绑定的支付宝账号进行提现) 96 | #aliName: "" 97 | #aliAccount: "" 98 | 99 | # 【选填】达到指定阅读篇数,走推送通道,默认为空 100 | # 功能描述:不想每一轮第一篇都走推送,则可以开启此配置项(可以自行增加数量,不一定固定两个) 101 | # 配置方式 1: 102 | #custom_detected_count: [1, 101, 151] 103 | # 配置方式 2: 104 | #custom_detected_count: 105 | # - 1 106 | # - 101 107 | # - 151 108 | 109 | # 【选填】第一次循环是否走推送 110 | # 功能描述:不管当前具体是第几轮第几篇,只要是刚运行程序,那么第一篇文章是否走推送 111 | first_while_to_push: False 112 | 113 | # 【必填】 114 | account_data: 115 | # 账号名(自定义) 116 | 账号1: 117 | # 【必填】 118 | cookie: "xyy_1" 119 | # 【必/选填】WxPusher中的用户UID,检测文章通知会单独发给这个微信用户 120 | # 功能描述:单独给此账号推送,微信要订阅 WxPusher的 -> 应用管理 -> 关注应用 121 | # 当 [设置了topicIds] 后,[uid可以注释/删掉] 122 | # 当 [未填写topicIds] 时,[uid必须填写](!!! 一定要在 【关注应用】中订阅 !!!) 123 | # uid: "1" 124 | # 【选填】自定义 user-agent,启用前请删掉 # 125 | # ua: "" 126 | 127 | -------------------------------------------------------------------------------- /schema/xyy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # xyy.py created by MoMingLog on 8/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-08 6 | 【功能描述】 7 | """ 8 | from typing import Type, Dict 9 | 10 | from pydantic import create_model, BaseModel, Field 11 | 12 | from schema.common import CommonPartConfig, CommonGlobalConfig 13 | 14 | class CommonYRYDConfig(BaseModel): 15 | withdraw_type: str = Field(None, description="提现类型: wx 微信, ali 支付宝") 16 | 17 | 18 | class XYYAccount(CommonPartConfig, CommonYRYDConfig): 19 | """账号配置(局部配置)""" 20 | cookie: str 21 | 22 | 23 | class BaseXYYGlobalConfig(CommonGlobalConfig, CommonYRYDConfig): 24 | """小阅阅 阅读配置(全局配置)""" 25 | biz_data: list | None = Field(None, description="检测文章的biz") 26 | 27 | 28 | # 通过 create_model() 方法创建动态键模型 29 | XYYConfig: Type[BaseXYYGlobalConfig] = create_model( 30 | 'XYYConfig', 31 | account_data=(Dict[str | int, XYYAccount], {}), 32 | source=(str, "xyy.yaml"), 33 | __base__=BaseXYYGlobalConfig 34 | ) 35 | 36 | 37 | class RspCommon(BaseModel): 38 | errcode: int 39 | msg: str | None = Field(None, description="响应信息") 40 | 41 | 42 | class GoldData(BaseModel): 43 | """金币数据""" 44 | gold: str | None = Field(None, description="此次阅读获取的金币数") 45 | last_gold: str | None = Field(None, description="上次金币数") 46 | day_read: str | None = Field(None, description="当天阅读数") 47 | day_gold: str | None = Field(None, description="当天金币数") 48 | remain_read: int | str | None = Field(None, description="剩余阅读数") 49 | 50 | def __str__(self): 51 | return "\n".join([ 52 | "【阅读收入情况】", 53 | f"> 剩余金币: {self.last_gold}", 54 | f"> 今日阅读: {self.day_read}", 55 | f"> 今日获得: {self.day_gold}", 56 | f"> 剩余篇数: {self.remain_read}" 57 | ]) 58 | 59 | def __repr__(self): 60 | return self.__str__() 61 | 62 | def get_read_result(self): 63 | return f"获得金币: {self.gold}, 今日共得: {self.day_gold}, 当前余额: {self.last_gold}" 64 | 65 | 66 | class Gold(RspCommon): 67 | """金币数据""" 68 | data: GoldData | None = Field(None, description="金币数据") 69 | 70 | def __str__(self): 71 | return self.data.__str__() 72 | 73 | def __repr__(self): 74 | return self.__str__() 75 | 76 | def get_read_result(self): 77 | return self.data.get_read_result() 78 | 79 | 80 | class WTMPDomainData(BaseModel): 81 | """跳转链接数据""" 82 | domain: str | None = Field(None, description="跳转链接") 83 | 84 | def __str__(self): 85 | return f"阅读跳转链接: {self.domain}" 86 | 87 | def __repr__(self): 88 | return self.__str__() 89 | 90 | 91 | class WTMPDomain(RspCommon): 92 | data: WTMPDomainData | None = Field(None, description="跳转链接数据") 93 | 94 | def __str__(self): 95 | return self.data.__str__() 96 | 97 | def __repr__(self): 98 | return self.__str__() 99 | 100 | 101 | class ArticleUrlData(BaseModel): 102 | link: str | None = Field(None, description="文章链接") 103 | type: str | None = Field(None, description="类型") 104 | a: str | None = Field(None, description="未知") 105 | 106 | def __str__(self): 107 | return f"文章链接: {self.link}" 108 | 109 | def __repr__(self): 110 | return self.__str__() 111 | 112 | 113 | class ArticleUrl(RspCommon): 114 | data: ArticleUrlData | None = Field(None, description="文章链接数据") 115 | 116 | def __str__(self): 117 | return self.data.__str__() 118 | 119 | def __repr__(self): 120 | return self.__str__() 121 | -------------------------------------------------------------------------------- /schema/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # common.py created by MoMingLog on 30/3/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-03-30 6 | 【功能描述】 7 | """ 8 | 9 | from pydantic import BaseModel, Field 10 | 11 | 12 | class CommonDelayConfig(BaseModel): 13 | """延迟配置""" 14 | read_delay: list | None = Field(None, description="阅读延迟时间(单位: 秒)") 15 | push_delay: list | None = Field(None, description="推送延迟时间(单位: 秒)") 16 | 17 | 18 | class CommonConfig(BaseModel): 19 | """相同的全局和局部配置(任务和任务账号配置)""" 20 | is_use_ws: bool = Field(None, description="是否使用websocket“回调”") 21 | ws_host: str | None = Field(None, description="websocket主机地址") 22 | init_colorama: bool = Field(True, 23 | description="是否初始化colorama,当此项为True青龙面板的颜色渲染会消失,这样或许可以避免不支持颜色显示的青龙面板出现乱码的现象") 24 | is_withdraw: bool | None = Field(None, description="是否进行提现操作") 25 | withdraw: float = Field(0, description="提现金额(单位: 元),表示只有大于等于这个数才可以提现") 26 | aliAccount: str | None = Field(None, description="支付宝账号,默认为空") 27 | aliName: str | None = Field(None, description="支付宝账号姓名,默认为空") 28 | ua: str | None = Field(None, description="用户浏览器标识") 29 | delay: CommonDelayConfig = Field(None, description="阅读延迟时间(单位: 秒)") 30 | wait_next_read: bool | None = Field(None, description="是否自动等待下批阅读") 31 | custom_detected_count: list | None = Field(None, description="达到指定阅读篇数走推送通道") 32 | first_while_to_push: bool | None = Field(None, description="第一次循环走推送") 33 | max_failed_pass_count: int | None = Field(None, description="最大检测未通过账号数量") 34 | push_types: list | None = Field(None, description="推送通道类型 1: WxPusher 2: WxBusinessPusher") 35 | # WxPusher 36 | appToken: str | None = Field(None, description="WxPusher推送通知的appToken") 37 | topicIds: str | list | None = Field(None, description="WxPusher推送通知的topicIds") 38 | # WxBusinessPusher Robot 39 | use_robot: bool | None = Field(None, description="是否使用企业微信机器人推送") 40 | is_push_markdown: bool | None = Field(None, description="是否推送MarkDown格式") 41 | webhook_url: str | None = Field(None, description="企业微信机器人推送通知的webhook_url") 42 | # WxBusinessPusher 43 | corp_id: str | None = Field(None, description="企业微信推送通知的企业ID") 44 | corp_secret: str | None = Field(None, description="企业微信推送通知的应用密钥") 45 | agent_id: int | None = Field(None, description="企业微信推送通知的应用ID") 46 | 47 | 48 | class CommonPartConfig(CommonConfig): 49 | """相同的局部配置(账号配置)""" 50 | uid: str | None = Field(None, description="账号ID") 51 | 52 | 53 | class CommonGlobalConfig(CommonConfig): 54 | """相同的全局配置(任务配置)""" 55 | debug: bool = Field(False, description="是否开启调试模式") 56 | strategy: int = Field( 57 | 2, 58 | description="配置全局优先策略(仅限全局配置,不包括账号局部配置) 1: 全局优先 2: 任务配置优先)" 59 | ) 60 | max_thread_count: int = Field(1, description="线程池中的最大线程数") 61 | is_log_response: bool | None = Field(None, description="是否打印响应日志") 62 | 63 | 64 | class ArticleInfo(BaseModel): 65 | """文章信息""" 66 | article_url: str 67 | article_biz: str 68 | article_title: str 69 | article_author: str 70 | article_desc: str 71 | 72 | def __str__(self): 73 | msg = [] 74 | if self.article_biz: 75 | msg.append(f"> 文章BIZ: {self.article_biz}") 76 | if self.article_url: 77 | msg.append(f"> 文章链接: {self.article_url}") 78 | if self.article_title: 79 | msg.append(f"> 文章标题: {self.article_title}") 80 | if self.article_author: 81 | msg.append(f"> 文章作者: {self.article_author}") 82 | if self.article_desc: 83 | msg.append(f"> 文章描述: {self.article_desc}") 84 | return "\n".join(msg) 85 | 86 | def __repr__(self): 87 | return self.__str__() 88 | -------------------------------------------------------------------------------- /config/yrrd_example.yaml: -------------------------------------------------------------------------------- 1 | # 【注意】 2 | # 1. “#”符号表示注释,被注释掉的配置是【不会生效】,如果想让其生效,请去掉它 3 | # 2. 请确保格式正确,否则会报错(只需要删除一个符号即可,其他的空格不用删除) 4 | # 3. 实在不理解的可以前往 https://t.me/+A6PlLqiqK19mNzNl 问一问其他已了解的伙伴 5 | # 4. 标注了“可单独配置”的,皆可以在对应的账号下添加,优先级更高 6 | 7 | # 【选填】推送方式,1: WxPusher 2: 企业微信(可单独配置) 8 | # 功能描述:选择对应的推送平台,推送检测文章信息(可多选) 9 | # Tips: 默认推送方式为 WxPusher 10 | # 方法 1: 11 | #push_types: [1, 2, 3] 12 | # 方法 2: 13 | #push_types: 14 | # - 1 15 | # - 2 16 | # - 3 17 | 18 | # --------------WxPusher配置------------------- 19 | # 【必/选填】 WxPusher推送通知的appToken(可单独配置) 20 | # 功能描述:当出现检测文章链接时,自动推送到对应账号 21 | # Tips: 可以在云桌面搭建微信机器人,用来自动监听此消息,从而完成自动过检测功能 22 | # Tips: 可在common.yaml为所有任务配置统一的appToken,此时这里的appToken可以省略 23 | # Tips: 由于默认【配置策略】为【任务优先】,也就是此文件配置优先 24 | # 故,如果这里是空字符串,则会覆盖common.yaml中的【配置策略】 25 | #appToken: "" 26 | 27 | # 【选填】WxPusher中的主题ID(可单独配置),下面两种方式选择一个即可 28 | # 配置方式 1:字符串 (此方式只能设置一个主题) 29 | #topicIds: "123456" 30 | # 配置方式2:数组 (此方式可以设置多个主题) 31 | #topicIds: ["123456", "789123"] 32 | # 或者下方数组格式 33 | #topicIds: 34 | # - "123456" 35 | # - "789123" 36 | # --------------WxPusher配置------------------- 37 | 38 | # --------------企业微信配置------------------- 39 | # 【选填】 是否使用 企业微信机器人推送(可单独配置) 40 | # 功能描述:当 push_type为 2 时生效,默认为True 41 | #use_robot: true 42 | # 【选填】 是否使用 markdown 格式文本推送,默认不使用,仅推送文章链接 43 | # 功能描述:markdown格式更好看,但是有的微信可能不支持(群友反馈) 44 | #is_push_markdown: false 45 | # 【必/选填】 企业微信机器人的webhook地址(可单独配置) 46 | # 功能描述:当 use_robot为 true 时生效 47 | #webhook_url: "https://..." 48 | 49 | # 【必/选填】企业ID 50 | # 功能描述:当 push_types==2 且 use_robot==true 时生效,此时此项必填 51 | #corp_id: "123456" 52 | # 【必/选填】应用密钥 53 | # 功能描述: 当 push_types==2 且 use_robot==true 时生效,此时此项必填 54 | #corp_secret: "123456" 55 | # 【必/选填】应用ID 56 | # 功能描述: 当 push_types==2 且 use_robot==true 时生效,此时此项必填 57 | #agent_id: 123456 58 | # --------------企业微信配置------------------- 59 | 60 | # 【选填】 全局延迟配置(可单独配置,直接在对应的账号中添加相同的格式即可) 61 | # 功能描述:完成阅读,获取阅读文章前的睡眠时间 62 | #delay: 63 | # # 模拟阅读时间,默认10-20 64 | # # Tips: 最小不能低于6秒 65 | # read_delay: 66 | # - 10 67 | # - 20 68 | # # 推送检测文章后的等待时间 69 | # push_delay: 70 | # - 19 71 | 72 | # 【选填】自动等待下批文章阅读(可单独配置),默认为 false 73 | # 功能描述:出现如:"下一批文章将在42分钟后到来"的消息后,是否自动睡眠 74 | #wait_next_read: true 75 | # 【选填】 最大线程数,默认为 1 76 | # 功能描述:设置线程池的最大线程数,每个线程可以单独跑一个账号 77 | #max_thread_count: 1 78 | 79 | # 【选填】是否需要提现,默认为 true,表示需要(可单独配置) 80 | # 功能描述:可以用于阅读信息获取任务,限制提现请求 81 | is_withdraw: true 82 | 83 | # 【选填】提现额度限制(可单独配置) 84 | # 功能描述:只有达到或者大于这个金额才允许提现 85 | #withdraw: 0.9 86 | 87 | # 【选填】提现方式:wx or ali (可单独配置) 88 | # 功能描述:wx表示微信提现,ali表示支付宝提现 89 | #withdraw_type: wx 90 | # 【选填】 全局的支付宝提现账号(可单独配置),默认为 None 91 | # 功能描述:先在上方选择 ali,再在此处填写对应的支付宝账号 92 | # Tips: 如果这里不填写,则会使用默认已经绑定的支付宝(如果账号已经绑定过,这里就算填写其他的,也会使用账号绑定的支付宝账号进行提现) 93 | #aliName: "" 94 | #aliAccount: "" 95 | 96 | # 【选填】达到指定阅读篇数,走推送通道,默认为空 97 | # 功能描述:不想每一轮第一篇都走推送,则可以开启此配置项(可以自行增加数量,不一定固定两个) 98 | # 配置方式 1: 99 | #custom_detected_count: [1, 101, 151] 100 | # 配置方式 2: 101 | #custom_detected_count: 102 | # - 1 103 | # - 101 104 | # - 151 105 | 106 | # 【选填】第一次循环是否走推送 107 | # 功能描述:不管当前具体是第几轮第几篇,只要是刚运行程序,那么第一篇文章是否走推送 108 | first_while_to_push: False 109 | 110 | # 【必填】 111 | account_data: 112 | # 账号名(自定义) 113 | 账号1: 114 | # 【必填】 115 | # 抓包简述:开启抓包后,进入主页面,再回到抓包软件,筛选出链接中包含 get_read_url 或者 tuijian,复制其中的cookie值即可 116 | # 貌似通过 PC微信 抓取的Cookie会出现过期现象,推荐抓取微信APP的Cookie 117 | # 无 root 的可以使用虚拟机,然后在虚拟机中登录微信APP,只需要抓取虚拟机的数据包即可 118 | cookie: "klyd_1" 119 | # 【必/选填】WxPusher中的用户UID,检测文章通知会单独发给这个微信用户 120 | # 功能描述:单独给此账号推送,微信要订阅 WxPusher的 -> 应用管理 -> 关注应用 121 | # 当 [设置了topicIds] 后,[uid可以注释/删掉] 122 | # 当 [未填写topicIds] 时,[uid必须填写](!!! 一定要在 【关注应用】中订阅 !!!) 123 | # uid: "1" 124 | # 【选填】自动等待下批文章阅读(优先级比全局配置高) 125 | # wait_next_read: true 126 | # 【选填】自定义 user-agent 127 | # ua: "" 128 | 129 | # 账号2: 130 | # cookie: "klyd_2" 131 | # uid: "2" 132 | # 账号3: 133 | # cookie: "klyd_3" 134 | # uid: "3" -------------------------------------------------------------------------------- /config/klyd_example.yaml: -------------------------------------------------------------------------------- 1 | # 【注意】 2 | # 1. “#”符号表示注释,被注释掉的配置是【不会生效】,如果想让其生效,请去掉它 3 | # 2. 请确保格式正确,否则会报错(只需要删除一个符号即可,其他的空格不用删除) 4 | # 3. 实在不理解的可以前往 https://t.me/+A6PlLqiqK19mNzNl 问一问其他已了解的伙伴 5 | # 4. 标注了“可单独配置”的,皆可以在对应的账号下添加,优先级更高 6 | 7 | # 【选填】推送方式,1: WxPusher 2: 企业微信(可单独配置) 8 | # 功能描述:选择对应的推送平台,推送检测文章信息(可多选) 9 | # Tips: 默认推送方式为 WxPusher 10 | # 方法 1: 11 | #push_types: [1, 2, 3] 12 | # 方法 2: 13 | #push_types: 14 | # - 1 15 | # - 2 16 | # - 3 17 | 18 | # --------------WxPusher配置------------------- 19 | # 【必/选填】 WxPusher推送通知的appToken(可单独配置) 20 | # 功能描述:当出现检测文章链接时,自动推送到对应账号 21 | # Tips: 可以在云桌面搭建微信机器人,用来自动监听此消息,从而完成自动过检测功能 22 | # Tips: 可在common.yaml为所有任务配置统一的appToken,此时这里的appToken可以省略 23 | # Tips: 由于默认【配置策略】为【任务优先】,也就是此文件配置优先 24 | # 故,如果这里是空字符串,则会覆盖common.yaml中的【配置策略】 25 | #appToken: "" 26 | 27 | # 【选填】WxPusher中的主题ID(可单独配置),下面两种方式选择一个即可 28 | # 配置方式 1:字符串 (此方式只能设置一个主题) 29 | #topicIds: "123456" 30 | # 配置方式2:数组 (此方式可以设置多个主题) 31 | #topicIds: ["123456", "789123"] 32 | # 或者下方数组格式 33 | #topicIds: 34 | # - "123456" 35 | # - "789123" 36 | # --------------WxPusher配置------------------- 37 | 38 | # --------------企业微信配置------------------- 39 | # 【选填】 是否使用 企业微信机器人推送(可单独配置) 40 | # 功能描述:当 push_type为 2 时生效,默认为True 41 | #use_robot: true 42 | # 【选填】 是否使用 markdown 格式文本推送,默认不使用,仅推送文章链接 43 | # 功能描述:markdown格式更好看,但是有的微信可能不支持(群友反馈) 44 | #is_push_markdown: false 45 | # 【必/选填】 企业微信机器人的webhook地址(可单独配置) 46 | # 功能描述:当 use_robot为 true 时生效 47 | #webhook_url: "https://..." 48 | 49 | # 【必/选填】企业ID 50 | # 功能描述:当 push_types==2 且 use_robot==true 时生效,此时此项必填 51 | #corp_id: "123456" 52 | # 【必/选填】应用密钥 53 | # 功能描述: 当 push_types==2 且 use_robot==true 时生效,此时此项必填 54 | #corp_secret: "123456" 55 | # 【必/选填】应用ID 56 | # 功能描述: 当 push_types==2 且 use_robot==true 时生效,此时此项必填 57 | #agent_id: 123456 58 | # --------------企业微信配置------------------- 59 | 60 | # 【选填】 全局延迟配置(可单独配置,直接在对应的账号中添加相同的格式即可) 61 | # 功能描述:完成阅读,获取阅读文章前的睡眠时间 62 | #delay: 63 | # # 模拟阅读时间,默认10-20 64 | # # Tips: 最小不能低于6秒 65 | # read_delay: 66 | # - 10 67 | # - 20 68 | # # 推送检测文章后的等待时间 69 | # push_delay: 70 | # - 19 71 | 72 | # 【选填】自动等待下批文章阅读(可单独配置),默认为 false 73 | # 功能描述:出现如:"下一批文章将在42分钟后到来"的消息后,是否自动睡眠 74 | #wait_next_read: true 75 | # 【选填】 最大线程数,默认为 1 76 | # 功能描述:设置线程池的最大线程数,每个线程可以单独跑一个账号 77 | #max_thread_count: 1 78 | 79 | # 【选填】是否需要提现,默认为 true,表示需要 (可单独配置) 80 | # 功能描述:可以用于阅读信息获取任务,限制提现请求 81 | is_withdraw: true 82 | 83 | # 【选填】提现额度限制(可单独配置) 84 | # 功能描述:只有达到或者大于这个金额才允许提现 85 | #withdraw: 0.9 86 | 87 | # 【选填】提现方式:wx or ali (可单独配置) 88 | # 功能描述:wx表示微信提现,ali表示支付宝提现 89 | #withdraw_type: wx 90 | # 【选填】 全局的支付宝提现账号(可单独配置),默认为 None 91 | # 功能描述:先在上方选择 ali,再在此处填写对应的支付宝账号 92 | # Tips: 如果这里不填写,则会使用默认已经绑定的支付宝(如果账号已经绑定过,这里就算填写其他的,也会使用账号绑定的支付宝账号进行提现) 93 | #aliName: "" 94 | #aliAccount: "" 95 | 96 | # 【选填】“以防万一”开关 (可单独配置) 97 | # 功能描述:第2篇文章是否继续推送(当前一篇文章被检测出来是检测文章,那么后一篇文章是否继续推送?) 98 | just_in_case: true 99 | 100 | # 【选填】“未知走推送”开关(可单独配置),为避免频繁推送,此功能默认关闭 101 | # 功能描述:如下所示,此数据返回的结果检测状态未知,目前只能通过匹配特征biz的方式来筛选 102 | # 此功能开关,则是用来作出选择,当返回下方所示数据时,【是走推送,还是听天由命】 103 | # biz匹配在逻辑前方,所以如果真到了使用这个开关的时候,那么说明biz匹配失败 104 | #{ 105 | # "jkey": "MDAwMDAwMDAwM......", 106 | # "url": "https://mp.weixin.qq.com/s?__biz=Mzg2OTYyNDY1OQ==&mid=2247649861&idx=1&sn=f0216ebeec1edb6c30ba1ab54a6fec7d&scene=0#wechat_redirect" 107 | #} 108 | unknown_to_push: false 109 | 110 | # 【选填】达到指定阅读篇数,走推送通道,默认为空 111 | # 功能描述:不想每一轮第一篇都走推送,则可以开启此配置项(可以自行增加数量,不一定固定两个) 112 | # 配置方式 1: 113 | #custom_detected_count: [1, 101, 151] 114 | # 配置方式 2: 115 | #custom_detected_count: 116 | # - 1 117 | # - 101 118 | # - 151 119 | 120 | # 【选填】第一次循环是否走推送 121 | # 功能描述:不管当前具体是第几轮第几篇,只要是刚运行程序,那么第一篇文章是否走推送 122 | first_while_to_push: False 123 | 124 | # 【必填】 125 | account_data: 126 | # 账号名(自定义) 127 | 账号1: 128 | # 【必填】 129 | # 抓包简述:开启抓包后,进入主页面,再回到抓包软件,筛选出链接中包含 get_read_url 或者 tuijian,复制其中的cookie值即可 130 | # 貌似通过 PC微信 抓取的Cookie会出现过期现象,推荐抓取微信APP的Cookie 131 | # 无 root 的可以使用虚拟机,然后在虚拟机中登录微信APP,只需要抓取虚拟机的数据包即可 132 | cookie: "klyd_1" 133 | # 【必/选填】WxPusher中的用户UID,检测文章通知会单独发给这个微信用户 134 | # 功能描述:单独给此账号推送,微信要订阅 WxPusher的 -> 应用管理 -> 关注应用 135 | # 当 [设置了topicIds] 后,[uid可以注释/删掉] 136 | # 当 [未填写topicIds] 时,[uid必须填写](!!! 一定要在 【关注应用】中订阅 !!!) 137 | # uid: "1" 138 | # 【选填】自动等待下批文章阅读(优先级比全局配置高) 139 | # wait_next_read: true 140 | # 【选填】自定义 user-agent 141 | # ua: "" 142 | 143 | # 账号2: 144 | # cookie: "klyd_2" 145 | # uid: "2" 146 | # 账号3: 147 | # cookie: "klyd_3" 148 | # uid: "3" 149 | biz_data: 150 | - "MzkwNTY1MzYxOQ==" 151 | - "biz2" 152 | - "biz3" 153 | - "biz4" -------------------------------------------------------------------------------- /read_fastapi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # read_fastapi.py created by MoMingLog on 15/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-15 6 | 【功能描述】 7 | new Env("FastApi-配置自动化"); 8 | 0 0 5 * * * read_fastapi.py 9 | 10 | 使用docker容器的, 请将6699端口映射出来 11 | 12 | 只要你的ip/域名+端口,可以访问,并显示“可以开始使用啦”,那么就表示你配置好了 13 | 14 | 为了简单一点,故此API程序统一使用 WxPusher 推送,请填好对应的配置项 15 | 16 | 方式一:添加环境变量(推荐: 因为拉库会覆盖此文件配置),下方被 `` 包裹的就是环境变量名 17 | 方式二:本文件下方有,按照注释内容填写即可 18 | 19 | """ 20 | import os 21 | import re 22 | import subprocess 23 | import platform 24 | import time 25 | 26 | import uvicorn 27 | from fastapi import FastAPI 28 | from uvicorn.main import Server, Config 29 | 30 | from app import all_router 31 | 32 | app = FastAPI() 33 | app.include_router(all_router) 34 | 35 | 36 | @app.get("/") 37 | async def read_root(): 38 | return {"message": "可以开始使用啦,那么就表示你配置好了"} 39 | 40 | 41 | class FastAPIServer: 42 | def __init__(self, app, host='0.0.0.0', port=16699, app_module='read_fastapi:app', force_start=False): 43 | self.host = host 44 | self.port = port 45 | self.app_module = app_module 46 | self.app = app 47 | self.port_in_use = self.is_port_in_use() 48 | self.first_run = self.check_first_run() # 检查是否第一次运行 49 | self.force_start = force_start 50 | 51 | def start(self): 52 | """ 53 | 启动 FastAPI 服务器 54 | """ 55 | if not self.force_start and self.first_run: 56 | print("程序是第一次运行,正在检查端口是否被占用") 57 | if self.port_in_use: 58 | print(f"端口已被占用,请检查端口 {self.port} 是否正在运行重要程序, 如果不重要,请本代码文件中的 force_start=True") 59 | return 60 | else: 61 | if self.port_in_use: 62 | print("端口已被占用,尝试停止占用进程...") 63 | self.kill_process_by_port(self.port) 64 | if self.is_port_in_use(): 65 | print("无法停止占用进程,无法启动服务器!") 66 | return 67 | uvicorn.run(self.app, host=self.host, port=self.port) 68 | 69 | def stop(self): 70 | """ 71 | 停止 FastAPI 服务器 72 | """ 73 | config = Config(app=self.app_module, host=self.host, port=self.port, reload=True) 74 | server = Server(config) 75 | server.should_exit = True 76 | 77 | def is_port_in_use(self): 78 | """ 79 | 检查端口是否被占用 80 | """ 81 | if platform.system() == 'Windows': 82 | output = self.get_netstat_output() 83 | lines = output.split('\n') 84 | for line in lines: 85 | if f":{self.port}" in line: 86 | return True 87 | return False 88 | else: 89 | output = self.get_ss_output() 90 | lines = output.split('\n') 91 | for line in lines: 92 | if f":{self.port}" in line: 93 | return True 94 | return False 95 | 96 | def kill_process_by_port(self, port): 97 | """ 98 | 根据端口号杀死进程 99 | """ 100 | if platform.system() == 'Windows': 101 | output = self.get_netstat_output() 102 | lines = output.split('\n') 103 | for line in lines: 104 | if f":{port}" in line: 105 | parts = line.split() 106 | pid = int(parts[-1]) # 获取最后一列的 PID 107 | print(f"Killing process with PID {pid} using port {port}...") 108 | os.system(f'taskkill /F /PID {pid}') # 使用 taskkill 命令杀死进程 109 | time.sleep(1) # 等待1秒确保进程已经被杀死 110 | elif platform.system() == 'Linux': 111 | output = self.get_ss_output() 112 | lines = output.split('\n') 113 | for line in lines: 114 | if f":{port}" in line: 115 | match = re.search(r'pid=(\d+),', line) 116 | if match: 117 | pid = int(match.group(1)) 118 | print(f"Killing process with PID {pid} using port {port}...") 119 | os.system(f'kill -9 {pid}') 120 | time.sleep(1) # 等待1秒确保进程已经被杀死 121 | else: 122 | print("暂未适配该操作系统") 123 | 124 | def get_netstat_output(self): 125 | """ 126 | 获取 netstat 命令的输出结果 127 | """ 128 | output = subprocess.run(['netstat', '-ano'], capture_output=True, text=True) 129 | return output.stdout 130 | 131 | def get_ss_output(self): 132 | """ 133 | 获取 ss 命令的输出结果 134 | """ 135 | output = subprocess.run(['ss', '-anp'], capture_output=True, text=True) 136 | return output.stdout 137 | 138 | def check_first_run(self): 139 | """ 140 | 检查程序是否第一次运行 141 | """ 142 | if os.path.exists('first_run.txt'): 143 | return False 144 | else: 145 | with open('first_run.txt', 'w') as f: 146 | f.write('First run') 147 | return True 148 | 149 | 150 | if __name__ == '__main__': 151 | server = FastAPIServer(app, force_start=False) 152 | server.start() -------------------------------------------------------------------------------- /schema/mmkk.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # mmkk.py created by MoMingLog on 28/3/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-03-28 6 | 【功能描述】 7 | """ 8 | from typing import Dict, Type 9 | 10 | from pydantic import BaseModel, Field, create_model 11 | 12 | from schema.common import CommonGlobalConfig, CommonPartConfig 13 | 14 | __all__ = [ 15 | "MMKKConfig", 16 | "WorkInfoRsp", 17 | "UserRsp", 18 | "WTMPDomainRsp", 19 | "MKWenZhangRsp", 20 | "AddGoldsRsp", 21 | ] 22 | 23 | 24 | class MMKKAccount(CommonPartConfig): 25 | """账号配置(局部配置)""" 26 | cookie: str 27 | 28 | 29 | class BaseMMKKGlobalConfig(CommonGlobalConfig): 30 | """猫猫看看阅读配置(全局配置)""" 31 | biz_data: list 32 | 33 | 34 | # 通过 create_model() 方法创建动态键模型 35 | MMKKConfig: Type[BaseMMKKGlobalConfig] = create_model( 36 | 'MMKKConfig', 37 | account_data=(Dict[str | int, MMKKAccount], {}), 38 | source=(str, "mmkk.yaml"), 39 | __base__=BaseMMKKGlobalConfig 40 | ) 41 | 42 | 43 | class CommonRsp(BaseModel): 44 | errcode: int 45 | msg: str 46 | 47 | 48 | class WorkInfoData(BaseModel): 49 | dayreads: int = Field(..., description="今天阅读的文章篇数") 50 | gold: int = Field(..., description="今天获得的金币数") 51 | remain_gold: str = Field(..., description="当前的金币数量") 52 | remain: float = Field(..., description="当前现金余额(元)") 53 | 54 | 55 | class WorkInfoRsp(CommonRsp): 56 | data: WorkInfoData | None = Field(None, description="今日统计信息") 57 | 58 | def __str__(self): 59 | # return f"【今日统计信息】\n> 阅读文章数: {self.data.dayreads}\n> 获得金币数: {self.data.gold}\n> 当前金币数: {self.data.remain_gold}\n> 当前余额(元): {self.data.remain}" 60 | if self.data: 61 | return "\n".join([ 62 | f"【今日统计信息】", 63 | f"❄️>> 阅读文章数: {self.data.dayreads}", 64 | f"❄️>> 获得金币数: {self.data.gold}", 65 | f"❄️>> 当前金币数: {self.data.remain_gold}", 66 | f"❄️>> 当前余额(元): {self.data.remain}" 67 | ]) 68 | return self.msg 69 | 70 | def __repr__(self): 71 | return self.__str__() 72 | 73 | 74 | class UserData(BaseModel): 75 | userid: str = Field(..., description="用户ID") 76 | addtime: str | int = Field(..., description="注册时间") 77 | adddate: str = Field(..., description="注册日期") 78 | partner_id: str = Field(..., description="") 79 | pid: str = Field(..., description="") 80 | note: str = Field(..., description="备注") 81 | 82 | 83 | class UserRsp(CommonRsp): 84 | data: UserData | None = Field(None, description="账号注册信息") 85 | 86 | def __str__(self): 87 | # return f"【账号注册信息】\n> 账号唯一ID: {self.data.userid}\n> 注册时间: {self.data.addtime}\n> 注册日期: {self.data.adddate}" 88 | if self.data: 89 | return "\n".join([ 90 | f"【账号注册信息】", 91 | f"❄️>> 账号唯一ID: {self.data.userid}", 92 | f"❄️>> 注册时间: {self.data.addtime}", 93 | f"❄️>> 注册日期: {self.data.adddate}" 94 | ]) 95 | 96 | def __repr__(self): 97 | return self.__str__() 98 | 99 | 100 | class WTMPDomainData(BaseModel): 101 | domain: str = Field(..., description="域名链接(返回的是完整的链接)") 102 | 103 | 104 | class WTMPDomainRsp(CommonRsp): 105 | data: WTMPDomainData | None = Field(None, description="阅读二维码链接") 106 | 107 | def __str__(self): 108 | # return f"【阅读二维码链接】\n> 阅读二维码链接: {self.data.domain}" 109 | return "\n".join([ 110 | f"【阅读二维码链接】", 111 | f"❄️>> 阅读二维码链接: {self.data.domain}" 112 | ]) 113 | 114 | def __repr__(self): 115 | return self.__str__() 116 | 117 | 118 | class MKWenZhangData(BaseModel): 119 | link: str | None = Field(None, description="阅读文章链接") 120 | type: str | None = Field(None, description="类型:目前已知的有read") 121 | type2: str | None = Field(None, 122 | description="类型: 目前已知的有read,有的响应体中没有这个参数,故设置默认值为空字符串") 123 | 124 | 125 | class MKWenZhangRsp(CommonRsp): 126 | data: MKWenZhangData | None = Field(None, description="阅读文章链接") 127 | 128 | def __str__(self): 129 | return f"【文章链接】\n❄️>> {self.data.link}" 130 | 131 | def __repr__(self): 132 | return self.__str__() 133 | 134 | 135 | class AddGoldsData(BaseModel): 136 | gold: int = Field(..., description="获得金币数") 137 | day_read: int = Field(..., description="今日阅读文章数") 138 | day_gold: int = Field(..., description="今日获得金币数") 139 | last_gold: int = Field(..., description="当前账户金币数") 140 | remain_read: int = Field(..., description="今日剩余阅读文章数") 141 | 142 | 143 | class AddGoldsRsp(CommonRsp): 144 | data: AddGoldsData | None = Field(None, description="阅读统计信息") 145 | 146 | def __str__(self): 147 | # return f"【阅读信息统计】\n> 获得金币数: {self.data.gold}\n> 今日阅读文章数: {self.data.day_read}\n> 今日获得金币数: {self.data.day_gold}\n> 当前账户金币数: {self.data.last_gold}\n> 今日剩余阅读文章数: {self.data.remain_read}" 148 | return "\n".join([ 149 | f"【阅读信息统计】", 150 | f"❄️>> 获得金币数: {self.data.gold}", 151 | f"❄️>> 今日阅读文章数: {self.data.day_read}", 152 | f"❄️>> 今日获得金币数: {self.data.day_gold}", 153 | f"❄️>> 当前账户金币数: {self.data.last_gold}", 154 | f"❄️>> 今日剩余阅读文章数: {self.data.remain_read}" 155 | ]) 156 | 157 | def __repr__(self): 158 | return self.__str__() 159 | -------------------------------------------------------------------------------- /script/v2/ddz_v2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ddz_v2.py created by MoMingLog on 17/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-17 6 | 【功能描述】 7 | """ 8 | import re 9 | 10 | from config import load_ddz_config 11 | from exception.common import RegExpError, APIChanged 12 | from schema.ddz import DDZConfig, DDZAccount, RspQrCode 13 | from script.common.base import WxReadTaskBase 14 | from utils import hide_dynamic_middle, md5 15 | 16 | 17 | class APIS: 18 | COMMON = "/index/mob" 19 | 20 | # API: 主页 21 | HOMEPAGE = f"{COMMON}/index.html?tt=1" 22 | # API: 阅读二维码 23 | READ_QRCODE = f"{COMMON}/get_read_qr.html" 24 | # API: 点赞看二维码 25 | LIKE_QRCODE = f"{COMMON}/get_zan_qr.html" 26 | 27 | 28 | class DDZV2(WxReadTaskBase): 29 | # 当前脚本作者 30 | CURRENT_SCRIPT_AUTHOR = "MoMingLog" 31 | # 当前脚本版本 32 | CURRENT_SCRIPT_VERSION = "2.0.0" 33 | # 当前脚本创建时间 34 | CURRENT_SCRIPT_CREATED = "2024-04-17" 35 | # 当前脚本更新时间 36 | CURRENT_SCRIPT_UPDATED = "2024-04-17" 37 | # 当前任务名称 38 | CURRENT_TASK_NAME = "点点赚" 39 | 40 | HOMEPAGE_CONTENT_COMPILE = re.compile( 41 | r"div.*?nickname.*?>(.*?)(.*?).*?(可用积分.*?) RspQrCode | dict: 74 | return self.request_for_json( 75 | "GET", 76 | APIS.LIKE_QRCODE, 77 | "获取点赞看二维码链接 base_client", 78 | client=self.base_client, 79 | model=RspQrCode 80 | ) 81 | 82 | def __request_read_qr_url(self) -> RspQrCode | dict: 83 | return self.request_for_json( 84 | "GET", 85 | APIS.READ_QRCODE, 86 | "获取阅读二维码链接 base_client", 87 | client=self.base_client, 88 | model=RspQrCode 89 | ) 90 | 91 | def __parse_homepage(self, homepage_html): 92 | if r := self.HOMEPAGE_CONTENT_COMPILE.search(homepage_html): 93 | if len(r.groups()) != 6: 94 | raise RegExpError(self.HOMEPAGE_CONTENT_COMPILE) 95 | 96 | self.phone = r.group(5) 97 | self.pwd = r.group(6) 98 | 99 | self.logger.info("\n".join([ 100 | "【用户信息】", 101 | f"❄️>> 用户昵称: {r.group(1).strip()}", 102 | f"❄️>> {r.group(2).strip()}", 103 | f"❄️>> {r.group(3)}", 104 | f"❄️>> {r.group(4)}", 105 | f"❄️>> 手机号: {hide_dynamic_middle(self.phone)}(已自动隐藏部分内容)", 106 | f"❄️>> 密码: {hide_dynamic_middle(self.pwd)}(已自动隐藏部分内容)" 107 | ])) 108 | else: 109 | raise RegExpError(self.HOMEPAGE_CONTENT_COMPILE) 110 | 111 | if r := self.QR_CODE_COMPILE.findall(homepage_html): 112 | if len(r) != 2: 113 | raise RegExpError(self.QR_CODE_COMPILE) 114 | 115 | read_qr_api = r[0] 116 | like_qr_api = r[1] 117 | 118 | if "19a07dface972ecd96546da6cc5052c8" != md5(read_qr_api) or "cc47a0b8fb0666b6f973bc18adb8533f" != md5( 119 | like_qr_api): 120 | raise APIChanged("获取二维码") 121 | 122 | APIS.READ_QRCODE = read_qr_api 123 | APIS.LIKE_QRCODE = like_qr_api 124 | self.logger.info("🟢 二维码API提取成功!") 125 | else: 126 | raise RegExpError(self.QR_CODE_COMPILE) 127 | 128 | def __request_homepage(self): 129 | return self.request_for_page( 130 | APIS.HOMEPAGE, 131 | "获取主页源代码", 132 | client=self.base_client 133 | ) 134 | 135 | def get_entry_url(self) -> str: 136 | return "http://qqd0vlfcop-185334769.baihu.sbs/index/center/poster.html?pid=61552" 137 | 138 | @property 139 | def phone(self): 140 | return self._cache.get(f"phone_{self.ident}") 141 | 142 | @phone.setter 143 | def phone(self, value): 144 | self._cache[f"phone_{self.ident}"] = value 145 | 146 | @property 147 | def pwd(self): 148 | return self._cache.get(f"pwd_{self.ident}") 149 | 150 | @pwd.setter 151 | def pwd(self, value): 152 | self._cache[f"pwd_{self.ident}"] = value 153 | 154 | @property 155 | def protocol(self): 156 | ret = self.account_config.protocol 157 | if ret is None: 158 | ret = self.config_data.protocol 159 | return ret if ret is not None else "http" 160 | 161 | @property 162 | def host(self): 163 | ret = self.account_config.host 164 | if ret is None: 165 | ret = self.config_data.host 166 | return ret if ret is not None else "28917700289.sx.shuxiangby.cn" 167 | 168 | @property 169 | def account_config(self) -> DDZAccount: 170 | return super().account_config 171 | 172 | 173 | if __name__ == '__main__': 174 | DDZV2() 175 | -------------------------------------------------------------------------------- /app/api/sniff_data/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __init__.py.py created by MoMingLog on 19/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-19 6 | 【功能描述】 7 | """ 8 | import json 9 | import os 10 | from typing import Dict 11 | 12 | import yaml 13 | from fastapi import APIRouter 14 | from pydantic import BaseModel, Field 15 | 16 | from config import sniff_dir 17 | from utils.push_utils import WxPusher 18 | 19 | sniff_data_router = APIRouter(tags=["配置自动化(处理上传的抓包数据)"]) 20 | 21 | 22 | class PostData(BaseModel): 23 | post_type: int = Field(..., description="上传数据的平台类型: 1:xyy 2:ltwm") 24 | user_id: int = Field(..., description="uid") 25 | user_name: str | None = Field(None, description="用户名") 26 | cookie: str | None = Field(None, alias="Cookie", description="Cookie") 27 | authorization: str | None = Field(None, alias="Authorization", description="Authorization") 28 | ua: str | None = Field(None, alias="User-Agent", description="User-Agent") 29 | host: str | None = Field(None, alias="Host", description="Host") 30 | protocol: str | None = Field(None, description="Protocol") 31 | 32 | 33 | def push_msg(msg: str, title: str = "MoMingLog-配置更新通知"): 34 | # WxPusher 配置 35 | # `WxPusher_AppToken` 36 | appToken = "" 37 | # `WxPusher_TopicIds` 主题ID,可直接配置一个,也可配置多个 38 | # 配置单个可以用 topicIds = "" 配置多个可以用 uids = ["topicId1", "topicId2"] 39 | # 环境变量支持多行,一行一个 40 | topicIds = [] 41 | # `WxPusher_UIDS` UIDS,可直接配置一个,也可配置多个 42 | # 配置单个可以用 uids = "uid" 配置多个可以用 uids = ["uid1", "uid2"] 43 | # 环境变量支持多行一行一个 44 | uids = "" 45 | 46 | if not appToken: 47 | appToken = os.getenv("WxPusher_AppToken") 48 | if not appToken: 49 | raise Exception("请配置环境变量 WxPusher_AppToken") 50 | 51 | if not topicIds: 52 | topicIds = os.getenv("WxPusher_TopicIds") 53 | 54 | if not uids: 55 | uids = os.getenv("WxPusher_UIDS") 56 | 57 | if not topicIds and not uids: 58 | raise Exception("请配置环境变量 WxPusher_TopicIds 或 WxPusher_UIDS") 59 | 60 | WxPusher.push_msg( 61 | appToken, 62 | title, 63 | msg, 64 | topicIds=topicIds, 65 | uids=uids 66 | ) 67 | 68 | 69 | config_dir = sniff_dir 70 | 71 | init_data = { 72 | 1: { 73 | "file_path": os.path.join(config_dir, "xyy.yaml"), 74 | "modify_name": "小阅阅阅读" 75 | }, 76 | 2: { 77 | "file_path": os.path.join(config_dir, "ltwm.yaml"), 78 | "modify_name": "力天微盟" 79 | }, 80 | 3: { 81 | "file_path": os.path.join(config_dir, "mmkk.yaml"), 82 | "modify_name": "猫猫看看" 83 | }, 84 | 4: { 85 | "file_path": os.path.join(config_dir, "yryd.yaml"), 86 | "modify_name": "鱼儿阅读" 87 | }, 88 | 5: { 89 | "file_path": os.path.join(config_dir, "klyd.yaml"), 90 | "modify_name": "可乐读书" 91 | }, 92 | 6: { 93 | "file_path": os.path.join(config_dir, "ddz.yaml"), 94 | "modify_name": "点点赚" 95 | } 96 | } 97 | 98 | 99 | @sniff_data_router.post("") 100 | async def _(data: PostData): 101 | if d := init_data.get(data.post_type): 102 | file_path = d.get("file_path") 103 | modify_name = d.get("modify_name") 104 | else: 105 | push_msg("当前平台暂不支持") 106 | return {"message": "当前平台暂不支持"} 107 | 108 | # 判断配置文件是否存在 109 | if not os.path.exists(file_path): 110 | os.makedirs(os.path.dirname(file_path), exist_ok=True) 111 | with open(file_path, "w", encoding="utf-8") as fp: 112 | fp.write("account_data:\n") 113 | 114 | # 先加载对应的yaml文件 115 | with open(file_path, "r", encoding="utf-8") as fp: 116 | try: 117 | # 为避免存储时保存不应该显示的配置项,故这里单独加载 118 | config_data = yaml.safe_load(fp) 119 | except (IOError, yaml.YAMLError): 120 | push_msg(f"配置文件 {file_path} 内容有误") 121 | return {"message": "配置文件内容有误"} 122 | 123 | # 获取文件中的account_data数据 124 | account_data: Dict[str, Dict] = config_data.get("account_data", {}) 125 | 126 | if account_data is None: 127 | account_data = {} 128 | 129 | user_data: Dict[str, Dict] = {} 130 | if data.user_name: 131 | key = data.user_name 132 | else: 133 | key = data.user_id 134 | 135 | update_user_name = None 136 | 137 | # 筛选出user_id相同的数据 138 | for account_name, account_info in account_data.items(): 139 | user_id = account_info.get("user_id") 140 | if user_id and user_id == data.user_id: 141 | update_user_name = account_name 142 | key = account_name 143 | user_data[key] = account_info 144 | break 145 | 146 | if not user_data: 147 | user_data[key] = {} 148 | old_account_data = "当前在添加新账号,故无原始数据!" 149 | else: 150 | old_account_data = json.dumps(user_data, ensure_ascii=False, indent=4) 151 | 152 | # 开始覆盖/添加配置 153 | if data.user_id: 154 | user_data[key]["user_id"] = data.user_id 155 | if data.cookie: 156 | user_data[key]["cookie"] = data.cookie 157 | if data.authorization: 158 | user_data[key]["authorization"] = data.authorization 159 | if data.ua: 160 | user_data[key]["ua"] = data.ua 161 | if data.host: 162 | user_data[key]["host"] = data.host 163 | if data.protocol: 164 | user_data[key]["protocol"] = data.protocol 165 | 166 | account_data.update(user_data) 167 | 168 | all_account_data = json.dumps(account_data, ensure_ascii=False, indent=4) 169 | 170 | update_content = json.dumps(user_data, ensure_ascii=False, indent=4) 171 | 172 | config_data["account_data"] = account_data 173 | 174 | with open(file_path, "w", encoding="utf-8") as fp: 175 | try: 176 | yaml.safe_dump( 177 | config_data, 178 | fp, 179 | encoding="utf-8", 180 | allow_unicode=True, 181 | sort_keys=False 182 | ) 183 | 184 | if update_user_name is None: 185 | header = f"{modify_name} - 新账号 添加成功!" 186 | else: 187 | header = f"{modify_name} - {update_user_name} 更新成功!" 188 | 189 | push_msg("\n".join([ 190 | header, 191 | f"> 原始数据内容如下:\n {old_account_data}", 192 | f"> 更新内容数据如下:\n {update_content}", 193 | f"> 所有账号数据如下:\n {all_account_data}" 194 | ])) 195 | return {"message": f"{modify_name} - 更新/添加成功!"} 196 | except (IOError, yaml.YAMLError): 197 | 198 | if update_user_name is None: 199 | header = f"{modify_name} - 新账号 添加失败!" 200 | else: 201 | header = f"{modify_name} - {update_user_name} 更新失败!" 202 | push_msg("\n".join([ 203 | header, 204 | f"> 原始数据内容如下:\n {old_account_data}", 205 | f"> 更新内容数据如下:\n {update_content}", 206 | f"> 所有账号数据如下:\n {all_account_data}" 207 | ])) 208 | 209 | return {"message": f"{modify_name} - 更新/添加失败!"} 210 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # __init__.py created by MoMingLog on 29/3/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-03-29 6 | 【功能描述】 7 | """ 8 | import os.path 9 | import shutil 10 | from typing import Type 11 | 12 | import yaml 13 | from pydantic import BaseModel 14 | 15 | from schema.ddz import DDZConfig 16 | from schema.klyd import KLYDConfig 17 | from schema.ltwm import LTWMConfig 18 | from schema.mmkk import MMKKConfig 19 | from schema.xyy import XYYConfig 20 | from schema.ymz import YMZConfig 21 | from schema.yryd import YRYDConfig 22 | from utils import md5 23 | 24 | root_dir = os.path.dirname(__file__) 25 | 26 | sniff_dir = os.path.join(root_dir, "sniff_config") 27 | 28 | 29 | def __load_config(task_name: str, filename: str, model: Type[BaseModel], **kwargs) -> any: 30 | """ 31 | 从本地加载数据 32 | :param filename: 文件名 33 | :return: 34 | """ 35 | common_path = os.path.join(root_dir, "common.yaml") 36 | 37 | sniff_file_path = os.path.join(sniff_dir, f"{filename}.yaml") 38 | 39 | file_path = os.path.join(root_dir, f"{filename}.yaml") 40 | 41 | biz_file_path = os.path.join(root_dir, "biz_data.yaml") 42 | 43 | biz_data = None 44 | 45 | if os.path.exists(biz_file_path): 46 | with open(biz_file_path, "r", encoding="utf-8") as fp: 47 | try: 48 | biz_data = yaml.safe_load(fp) 49 | except (IOError, yaml.YAMLError): 50 | pass 51 | 52 | example_file_path = os.path.join(root_dir, f"{filename}_example.yaml") 53 | 54 | if not os.path.exists(common_path): 55 | # 复制common_example.yaml, 作为common.yaml的模板 56 | shutil.copyfile(os.path.join(root_dir, f"common_example.yaml"), common_path) 57 | 58 | if not os.path.exists(file_path): 59 | msg = f"【{task_name}任务】配置文件不存在\n> 提示: 请在config文件夹下创建{filename}.yaml(参考{filename}_example.yaml文件)\n> 路径:{example_file_path}" 60 | raise FileNotFoundError(msg) 61 | 62 | with open(common_path, "r", encoding="utf-8") as fp: 63 | try: 64 | config_data = yaml.safe_load(fp) 65 | except (IOError, yaml.YAMLError): 66 | msg = f"【{task_name}任务】配置文件内容有误\n> 参考内容:{filename}_example.yaml\n> 路径:{example_file_path}" 67 | raise ValueError(msg) 68 | 69 | with open(file_path, "r", encoding="utf-8") as fp: 70 | data = yaml.safe_load(fp) 71 | 72 | if data is None: 73 | msg = f"【{task_name}任务】配置文件内容为空\n> 参考内容:{filename}_example.yaml\n> 路径:{example_file_path}" 74 | raise ValueError(msg) 75 | else: 76 | sniff_data = {} 77 | if os.path.exists(sniff_file_path): 78 | with open(sniff_file_path, "r", encoding="utf-8") as fp: 79 | sniff_data = yaml.safe_load(fp) 80 | config_data = data if config_data is None else config_data 81 | data.get("account_data", {}).update(sniff_data.get("account_data", {})) 82 | config_data.update(data) 83 | if (old_biz_data := config_data.get("biz_data")) and biz_data is not None: 84 | old_biz_data.extend(biz_data) 85 | config_data['biz_data'] = list(set(old_biz_data)) 86 | else: 87 | config_data['biz_data'] = biz_data 88 | 89 | return model(**config_data, source=file_path, **kwargs) 90 | 91 | 92 | def load_mmkk_config() -> MMKKConfig: 93 | """ 94 | 加载猫猫看看阅读的配置 95 | :return: 96 | """ 97 | return __load_config("猫猫看看", "mmkk", MMKKConfig) 98 | 99 | 100 | def load_klyd_config() -> KLYDConfig: 101 | """ 102 | 加载可乐阅读的配置 103 | :return: 104 | """ 105 | return __load_config("可乐阅读", "klyd", KLYDConfig) 106 | # if data.biz_data is None: 107 | # data.biz_data = ["MzkwNTY1MzYxOQ=="] 108 | # # 给yaml中没有配置biz_data的自动添加 109 | # with open(os.path.join(root_dir, "klyd.yaml"), "a", encoding="utf-8") as fp: 110 | # fp.write("\nbiz_data:\n") 111 | # for i in data.biz_data: 112 | # fp.write(f" - \"{i}\"\n") 113 | # return data 114 | 115 | 116 | def load_yryd_config() -> YRYDConfig: 117 | """ 118 | 加载鱼儿阅读的配置 119 | :return: 120 | """ 121 | return __load_config("鱼儿阅读", "yryd", YRYDConfig) 122 | 123 | 124 | def load_ltwm_config() -> LTWMConfig: 125 | """ 126 | 加载力天微盟阅读的配置 127 | :return: 128 | """ 129 | return __load_config("力天微盟", "ltwm", LTWMConfig) 130 | 131 | 132 | def load_xyy_config() -> XYYConfig: 133 | """ 134 | 加载 小阅阅 阅读的配置 135 | :return: 136 | """ 137 | return __load_config("小阅阅", "xyy", XYYConfig) 138 | 139 | 140 | def load_ddz_config() -> DDZConfig: 141 | """ 142 | 加载 点点赚 阅读的配置 143 | :return: 144 | """ 145 | return __load_config("点点赚", "ddz", DDZConfig) 146 | 147 | 148 | def load_ymz_config() -> YMZConfig: 149 | """ 150 | 加载 有米赚 阅读的配置 151 | :return: 152 | """ 153 | return __load_config("有米赚", "ymz", YMZConfig) 154 | 155 | 156 | cache_dir = os.path.join(root_dir, "cache") 157 | 158 | if not os.path.exists(cache_dir): 159 | os.makedirs(cache_dir) 160 | 161 | cache_file_path = os.path.join(root_dir, "cache", "cache.yaml") 162 | 163 | 164 | def storage_cache_config(cache_data: dict, file_path: str = None) -> None: 165 | """ 166 | 将新的缓存数据与现有数据合并后写入缓存配置文件中 167 | 168 | :param cache_data: 要合并的缓存数据 169 | :param file_path: 缓存配置文件的路径 170 | :return: None 171 | """ 172 | if file_path is None: 173 | file_path = cache_file_path 174 | try: 175 | if os.path.exists(file_path): 176 | with open(file_path, "r", encoding="utf-8") as f: 177 | existing_data = yaml.safe_load(f) or {} 178 | existing_data.update(cache_data) 179 | 180 | with open(file_path, "w", encoding="utf-8") as fp: 181 | yaml.dump(existing_data, fp) 182 | except (IOError, yaml.YAMLError) as e: 183 | print(f"缓存文件更新失败: {e}") 184 | 185 | 186 | def load_wx_business_access_token(corp_id: int, agent_id: int, file_path: str = None) -> str: 187 | """ 188 | 从缓存配置文件中加载指定corp_id和agent_id的access_token 189 | 190 | :param corp_id: 企业ID 191 | :param agent_id: 应用ID 192 | :param file_path: 缓存配置文件的路径 193 | :return: access_token 194 | """ 195 | try: 196 | if file_path is None: 197 | file_path = cache_file_path 198 | 199 | with open(file_path, "r", encoding="utf-8") as fp: 200 | cache_data = yaml.safe_load(fp) or {} 201 | 202 | key = md5(f"{corp_id}_{agent_id}") 203 | 204 | access_token = cache_data["wxBusiness"][key].get("accessToken") 205 | 206 | if access_token is None: 207 | raise KeyError(f"未找到对应配置项数据 corp_id={corp_id}, agent_id={agent_id}") 208 | 209 | return access_token 210 | except (IOError, yaml.YAMLError) as e: 211 | # 这里抛出KeyError异常,方便 推送方法 中的 在线获取token 212 | raise KeyError(f"缓存文件读取失败: {e}") 213 | 214 | 215 | detected_file_path = os.path.join(cache_dir, "detected.yaml") 216 | 217 | 218 | def load_detected_data() -> set: 219 | """读取检测数据,返回去重后的数据""" 220 | if not os.path.exists(detected_file_path): 221 | return set() 222 | try: 223 | with open(detected_file_path, "r", encoding="utf-8") as fp: 224 | data = yaml.safe_load(fp) 225 | return set(data) 226 | except (IOError, yaml.YAMLError) as e: 227 | print(f"检测数据加载失败: {e}") 228 | except TypeError: 229 | print("配置文件内容有误,请检查") 230 | 231 | 232 | def store_detected_data(new_data: set, old_data: set = None): 233 | if old_data is not None: 234 | old_data.update(new_data) 235 | else: 236 | old_data = new_data 237 | try: 238 | with open(detected_file_path, "w", encoding="utf-8") as fp: 239 | yaml.dump(list(old_data), fp) 240 | return True 241 | except (IOError, yaml.YAMLError) as e: 242 | print(f"检测数据更新失败: {e}") 243 | 244 | 245 | if __name__ == '__main__': 246 | print(load_mmkk_config().account_data) -------------------------------------------------------------------------------- /utils/entry_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 永久入口.py created by MoMingLog on 25/3/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-03-25 6 | 【功能描述】 7 | """ 8 | import re 9 | from concurrent.futures import ThreadPoolExecutor 10 | from typing import List 11 | from urllib.parse import urlparse 12 | 13 | import httpx 14 | 15 | 16 | class EntryUrl: 17 | """永久入口获取""" 18 | COMMON_URL_REG = re.compile(r'getCode.*?url.*?"(.*?)",', re.S) 19 | 20 | def __init__( 21 | self, 22 | *item_args: dict | List[dict], 23 | ): 24 | """ 25 | 初始化 26 | :param data: 提交数据 27 | :param page_type: 页面类型 28 | 类型0:表示还需要通过接口获取 29 | 类型1:页面会自动显示接口链接,这时直接提取即可 30 | 31 | """ 32 | headers = { 33 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.142.86 Safari/537.36", 34 | } 35 | self._client = httpx.Client(headers=headers, verify=False) 36 | self._result = {} 37 | with ThreadPoolExecutor(max_workers=5) as executor: 38 | futures = [] 39 | for _item in item_args: 40 | data_list = _item if isinstance(_item, list) else [_item] 41 | for d in data_list: 42 | future = executor.submit(self.run, d) 43 | futures.append((future, d)) 44 | 45 | for future, d in futures: 46 | result = future.result() 47 | if result: 48 | self._result[d.get("name")] = result 49 | 50 | def __dict__(self): 51 | return self._result 52 | 53 | def __str__(self): 54 | msg_list = [] 55 | for name, value in self._result.items(): 56 | msg_list.append(f"【平台名称】\n> {name}") 57 | msg_list.append(f"【获取结果】") 58 | if isinstance(value, str): 59 | msg_list.append(f"> {value}") 60 | elif isinstance(value, list): 61 | for v in value: 62 | if isinstance(v, tuple): 63 | msg_list.append(f"> {v[0]} -> {v[1]}") 64 | else: 65 | msg_list.append(f"> {v}") 66 | msg_list.append("") 67 | return "\n".join(msg_list) 68 | 69 | def __repr__(self): 70 | return self.__str__() 71 | 72 | def run(self, data: dict): 73 | task_name = data.get("name") 74 | # print(f"{task_name} 的永久入口为:{self.get_en_url(data)}") 75 | # self._result[task_name] = self.get_en_url(data) 76 | res = self.__get_en_url(data) 77 | return res 78 | 79 | def __get_en_url(self, data: dict): 80 | page_url = data.get("url") 81 | reg_str = data.get("reg") 82 | if reg_str is not None: 83 | page_compile = re.compile(reg_str, re.S) 84 | else: 85 | page_compile = EntryUrl.COMMON_URL_REG 86 | result = self.__fetch_fun(page_url, page_compile) 87 | if result is None: 88 | return 89 | page_type = data.get("type", 0) 90 | if page_type == 0: 91 | url = result[0] 92 | res_json = self.__request_json(url) 93 | if res_json["code"] == 0: 94 | r = res_json["data"]["luodi"] 95 | parse_url = urlparse(r) 96 | invite_url = data.get("invite_url") 97 | parse_invite_url = urlparse(invite_url) 98 | if invite_url: 99 | return invite_url.replace(parse_invite_url.netloc, parse_url.netloc) 100 | return r 101 | elif page_type == 1: 102 | return result 103 | 104 | def __fetch_fun(self, page_url, page_compile): 105 | html = self.__request_homepage(page_url) 106 | if html: 107 | return page_compile.findall(html) 108 | 109 | def __request_json(self, url): 110 | response = self._client.get(url) 111 | try: 112 | res_json = response.json() 113 | return res_json 114 | except: 115 | print(f"解析失败:{url}") 116 | 117 | def __request_homepage(self, url): 118 | try: 119 | return self._client.get(url).text 120 | except: 121 | print(f"请求失败:{url}") 122 | 123 | @classmethod 124 | def get_mmkk_entry_url(cls, url: str = None, invite_url: str = None) -> str: 125 | """ 126 | 获取猫猫看看阅读的入口链接 127 | :param url: 永久入口主页链接 128 | :param invite_url: 个人邀请链接 129 | :param ret_type: 返回值类型 130 | :return: 131 | """ 132 | if url is None: 133 | url = "https://code.sywjmlou.com.cn/" 134 | 135 | if invite_url is None: 136 | invite_url = "http://72484f04191524d9e5.atlfuhl.cn/mauth/f5097609e2ff70f696af4c1ed8b3ed4e" 137 | 138 | return EntryUrl({ 139 | "name": "猫猫看看", 140 | "url": url, 141 | "invite_url": invite_url 142 | }).all_entry_url 143 | 144 | @classmethod 145 | def get_xyy_entry_url(cls, url: str = None, invite_url: str = None) -> str: 146 | if url is None: 147 | url = "https://www.filesmej.cn/" 148 | 149 | if invite_url is None: 150 | invite_url = "http://9bk2.lvk72.shop/yunonline/v1/auth/5729acffb4b05596aef08e18eaf8a7cd?codeurl=9bk2.lvk72.shop&codeuserid=2&time=1711531311" 151 | 152 | return EntryUrl({ 153 | "name": "小阅阅", 154 | "url": url, 155 | "invite_url": invite_url 156 | }).all_entry_url 157 | 158 | @classmethod 159 | def get_klrd_entry_url(cls, url: str = None, invite_url: str = None) -> str: 160 | if url is None: 161 | url = "http://m.fbjcoru.cn/entry?upuid=1622410" 162 | return EntryUrl({ 163 | "name": "可乐读书", 164 | "url": url, 165 | "reg": r"(入口\d+).*?(http\S+?(?=\s|;|`))", 166 | "type": 1 167 | }).all_entry_url 168 | 169 | @classmethod 170 | def get_yryd_entry_url(cls, url: str = None, invite_url: str = None) -> str: 171 | if url is None: 172 | url = "http://h5.eqlrqqt.cn/entry/index5?upuid=2068422" 173 | 174 | return EntryUrl({ 175 | "name": "鱼儿阅读", 176 | "url": url, 177 | "reg": r"url_h51[.\s=']*(http\S+)'", 178 | "type": 1 179 | }).all_entry_url 180 | 181 | @classmethod 182 | def get_all_entry_url(cls, *data: dict | List[dict], is_flag: bool = False) -> list | dict: 183 | if not data: 184 | data = [{ 185 | "name": "小阅阅", 186 | "url": "https://www.filesmej.cn/", 187 | "invite_url": "http://9bk2.lvk72.shop/yunonline/v1/auth/5729acffb4b05596aef08e18eaf8a7cd?codeurl=9bk2.lvk72.shop&codeuserid=2&time=1711531311" 188 | }, { 189 | "name": "猫猫看看", 190 | "url": "https://code.sywjmlou.com.cn/", 191 | "invite_url": "http://72484f04191524d9e5.atlfuhl.cn/mauth/f5097609e2ff70f696af4c1ed8b3ed4e" 192 | }, { 193 | "name": "可乐读书", 194 | "url": "http://m.fbjcoru.cn/entry?upuid=1622410", 195 | "reg": r"(入口\d+).*?(http\S+?(?=\s|;|`))", 196 | "type": 1 197 | }, { 198 | "name": "鱼儿阅读", 199 | "url": "http://h5.eqlrqqt.cn/entry/index5?upuid=2068422", 200 | "reg": r"url_h51[.\s=']*(http\S+)'", 201 | "type": 1 202 | }] 203 | if is_flag: 204 | return EntryUrl(data)._result 205 | 206 | return EntryUrl(data).all_entry_url 207 | 208 | @property 209 | def all_entry_url(self) -> str | list: 210 | url_list = [] 211 | for value in self._result.values(): 212 | if isinstance(value, list): 213 | if len(value) == 1: 214 | url_list.append(value[0]) 215 | else: 216 | temp_tuple = () 217 | for n_v in value: 218 | temp_tuple += (n_v[1],) 219 | 220 | url_list.append(temp_tuple) 221 | else: 222 | url_list.append(value) 223 | if len(url_list) == 1: 224 | return url_list[0] 225 | return url_list 226 | 227 | 228 | if __name__ == "__main__": 229 | print(EntryUrl.get_all_entry_url()) 230 | -------------------------------------------------------------------------------- /schema/ltwm.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ltwm.py created by MoMingLog on 4/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-04 6 | 【功能描述】 7 | """ 8 | from typing import Type, Dict, List 9 | 10 | from pydantic import create_model, BaseModel, Field 11 | 12 | from schema.common import CommonPartConfig, CommonGlobalConfig 13 | from utils import is_date_after_today 14 | 15 | 16 | class LTWMAccount(CommonPartConfig): 17 | """力天微盟(局部配置)""" 18 | authorization: str 19 | 20 | 21 | class BaseLTWMGlobalConfig(CommonGlobalConfig): 22 | """力天微盟(全局配置)""" 23 | pass 24 | 25 | 26 | LTWMConfig: Type[BaseLTWMGlobalConfig] = create_model( 27 | "LTWMConfig", 28 | account_data=(Dict[str | int, LTWMAccount], {}), 29 | source=(str, "ltwm.yaml"), 30 | __base__=BaseLTWMGlobalConfig 31 | ) 32 | 33 | 34 | class CommonRsp(BaseModel): 35 | code: int 36 | businessCode: str 37 | message: str | None 38 | remark: str | None 39 | 40 | 41 | class UserPointInfoData(BaseModel): 42 | """用户积分信息""" 43 | balance: int 44 | totalIncome: int 45 | withdrawAmount: float 46 | 47 | def __str__(self): 48 | return "\n".join([ 49 | f"【用户积分信息】", 50 | f"> 可用积分: {self.balance} 积分", 51 | f"> 总 收 入: {self.totalIncome} 积分", 52 | f"> 已 兑 换: {self.withdrawAmount} 元" 53 | ]) 54 | 55 | def __repr__(self): 56 | return self.__str__() 57 | 58 | 59 | class UserPointInfo(CommonRsp): 60 | data: UserPointInfoData | None 61 | 62 | def __str__(self): 63 | return self.data.__str__() 64 | 65 | def __repr__(self): 66 | return self.__str__() 67 | 68 | 69 | class TaskListDataItem(BaseModel): 70 | """ 71 | 参考数据如下: 72 | "id": 1, 73 | "name": "文章阅读推荐", 74 | "code": 200, 75 | "taskKey": "209", 76 | "taskTag": "每日任务,多劳多得", 77 | "label": "单轮上限20篇,完成阅读后积分秒到账", 78 | "icon": "https://litianwm.oss-cn-hangzhou.aliyuncs.com/act/wx_article_read_xxhdpi.png?2", 79 | "data": "{\"cycleCount\":10,\"cycleMinute\":60,\"endHour\":20,\"startHour\":9}", 80 | "status": 2, 81 | "sortIndex": 1, 82 | "remark": "280积分/轮", 83 | "linkUrl": None, 84 | "taskRemainTime": 0 85 | """ 86 | id: int 87 | name: str 88 | code: int 89 | taskKey: str | None 90 | taskTag: str 91 | label: str 92 | icon: str 93 | data: str | None 94 | status: int 95 | sortIndex: int 96 | remark: str 97 | linkUrl: None | str 98 | taskRemainTime: int | None 99 | 100 | 101 | class TaskList(CommonRsp): 102 | """获取用户任务列表响应体""" 103 | data: List[TaskListDataItem] | None 104 | 105 | 106 | class ReaderDomain(CommonRsp): 107 | """获取阅读链接响应体""" 108 | data: str | None 109 | 110 | 111 | class GetTokenByWxKey(CommonRsp): 112 | """获取token""" 113 | data: str | None 114 | 115 | 116 | class ArticleUrlData(BaseModel): 117 | articleUrl: str | None = Field(..., description="下一篇文章的访问地址") 118 | taskKey: int = Field(..., description="任务key,目前作用不详") 119 | readKey: str = Field(..., description="下一篇阅读完成请求体中的内容数据") 120 | readSecond: int = Field(..., description="阅读的秒数") 121 | articleNum: int = Field(..., description="当前阅读的篇数") 122 | participateUserNum: int = Field(..., description="参与此任务的用户编号") 123 | 124 | def __str__(self): 125 | msg_list = [ 126 | f"【响应数据如下】", 127 | f"> 用户编号: {self.participateUserNum}", 128 | f"> 下篇链接: {self.articleUrl}", 129 | f"> taskKey: {self.taskKey}", 130 | f"> readKey: {self.readKey}", 131 | ] 132 | if self.readSecond > 0: 133 | msg_list.append(f"> 上篇阅读: {self.readSecond}秒") 134 | 135 | msg_list.append(f"> 已阅篇数: {self.articleNum}篇") 136 | return "\n".join(msg_list) 137 | 138 | def __repr__(self): 139 | return self.__str__() 140 | 141 | 142 | class ArticleUrl(CommonRsp): 143 | """获取文章地址响应体""" 144 | data: ArticleUrlData | None 145 | 146 | def __str__(self): 147 | return self.data.__str__() 148 | 149 | def __repr__(self): 150 | return self.__str__() 151 | 152 | 153 | class CompleteRead(CommonRsp): 154 | """阅读上报响应""" 155 | data: ArticleUrlData | None 156 | 157 | def __str__(self): 158 | return self.data.__str__() 159 | 160 | def __repr__(self): 161 | return self.__str__() 162 | 163 | 164 | class SignItem(BaseModel): 165 | date: str 166 | signed: bool 167 | integral: int 168 | 169 | def __str__(self): 170 | msg_list = [f"> 日期: {self.date}"] 171 | if self.signed: 172 | msg_list.append(f"> 状态:✅️ 已签") 173 | msg_list.append(f"> 已获积分: {self.integral}") 174 | else: 175 | if is_date_after_today(self.date): 176 | msg_list.append(f"> 状态: 🔶 待签") 177 | msg_list.append(f"> 待领积分: {self.integral}") 178 | else: 179 | msg_list.append(f"> 状态: ❌️ 漏签") 180 | msg_list.append(f"> 遗憾失去: {self.integral}") 181 | return "\n".join(msg_list) 182 | 183 | def __repr__(self): 184 | return self.__str__() 185 | 186 | 187 | class SignData(BaseModel): 188 | signItemList: List[SignItem] 189 | todayCanSign: bool = Field(..., description="今天是否可以签到") 190 | todaySigned: bool = Field(..., description="今天是否已经签到") 191 | currentIntegral: int = Field(..., description="当前签到应获得积分") 192 | nextIntegral: int = Field(..., description="下次签到应获得积分") 193 | 194 | def __str__(self): 195 | if self.todaySigned: 196 | return f"> 🟢 今日已签到! 获得{self.currentIntegral}积分,明日签到将获得{self.nextIntegral}积分" 197 | elif not self.todayCanSign: 198 | return "当前还不允许签到,可能未满足签到要求!" 199 | 200 | def __repr__(self): 201 | return self.__str__() 202 | 203 | def print_week_sign_status(self): 204 | for item in self.signItemList: 205 | print(item) 206 | print() 207 | 208 | 209 | class Sign(CommonRsp): 210 | """签到任务""" 211 | data: SignData | None 212 | 213 | 214 | class BalanceWithdrawData(BaseModel): 215 | exchangeBefore: int 216 | balance: int 217 | withdrawIntegral: int 218 | exchangeMoney: int 219 | progress: int | None 220 | minIntegralLimit: int 221 | 222 | def __str__(self): 223 | return "\n".join([ 224 | "【提现响应数据】", 225 | f"> 原始积分: {self.exchangeBefore}", 226 | f"> 剩余积分: {self.balance}", 227 | f"> 消耗积分: {self.withdrawIntegral}", 228 | f"> 已兑金额: {self.exchangeMoney}元" 229 | ]) 230 | 231 | def __repr__(self): 232 | return self.__str__() 233 | 234 | 235 | class BalanceWithdraw(CommonRsp): 236 | data: BalanceWithdrawData | None 237 | 238 | def __str__(self): 239 | return self.data.__str__() 240 | 241 | def __repr__(self): 242 | return self.__str__() 243 | 244 | 245 | if __name__ == '__main__': 246 | print(Sign.parse_obj({ 247 | "code": 200, 248 | "businessCode": "1", 249 | "message": "签到成功", 250 | "data": { 251 | "signItemList": [ 252 | { 253 | "date": "2024-04-05", 254 | "signed": True, 255 | "integral": 40 256 | }, 257 | { 258 | "date": "2024-04-06", 259 | "signed": False, 260 | "integral": 50 261 | }, 262 | { 263 | "date": "2024-04-07", 264 | "signed": False, 265 | "integral": 60 266 | }, 267 | { 268 | "date": "2024-04-08", 269 | "signed": False, 270 | "integral": 70 271 | }, 272 | { 273 | "date": "2024-04-09", 274 | "signed": False, 275 | "integral": 80 276 | }, 277 | { 278 | "date": "2024-04-10", 279 | "signed": False, 280 | "integral": 90 281 | }, 282 | { 283 | "date": "2024-04-11", 284 | "signed": False, 285 | "integral": 100 286 | } 287 | ], 288 | "todayCanSign": False, 289 | "todaySigned": True, 290 | "currentIntegral": 40, 291 | "nextIntegral": 50 292 | }, 293 | "remark": None 294 | }).data.print_week_sign_status()) 295 | -------------------------------------------------------------------------------- /schema/ymz.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ymz.py created by MoMingLog on 17/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-17 6 | 【功能描述】 7 | """ 8 | from typing import Type, Dict, List 9 | 10 | from pydantic import create_model, BaseModel, Field 11 | 12 | from schema.common import CommonPartConfig, CommonGlobalConfig 13 | 14 | 15 | class CommonYMZConfig(BaseModel): 16 | """有米赚全局和局部的相同配置""" 17 | pwd: int | None = Field(None, description="有米赚提现密码") 18 | 19 | 20 | class YMZAccount(CommonPartConfig, CommonYMZConfig): 21 | """有米赚(局部配置)""" 22 | userShowId: int 23 | 24 | 25 | class BaseYMZGlobalConfig(CommonGlobalConfig, CommonYMZConfig): 26 | """有米赚(全局配置)""" 27 | pass 28 | 29 | 30 | YMZConfig: Type[BaseYMZGlobalConfig] = create_model( 31 | "YMZConfig", 32 | account_data=(Dict[str | int, YMZAccount], {}), 33 | source=(str, "ymz.yaml"), 34 | __base__=BaseYMZGlobalConfig 35 | ) 36 | 37 | 38 | class CommonRsp(BaseModel): 39 | code: int 40 | message: str 41 | success: bool | None = Field(None, description="是否成功") 42 | 43 | 44 | class RspLoginInfo(BaseModel): 45 | accountId: int | None = Field(None, description="上级ID") 46 | city: str | None = Field(None, description="城市") 47 | country: str | None = Field(None, description="国家") 48 | createTime: str | None = Field(None, description="创建时间") 49 | createUser: int | None = Field(None, description="创建人") 50 | daySum: str | None = Field(None, description="日总量") 51 | deductNum: int | None = Field(None, description="扣量") 52 | delFlag: int | None = Field(None, description="删除标记") 53 | drawMoney: int | None = Field(None, description="提现") 54 | headimgurl: str | None = Field(None, description="头像") 55 | highQuality: int | None = Field(None, description="高清") 56 | id: str | None = Field(None, description="账号ID") 57 | isPwd: bool | None = Field(None, description="是否有密码") 58 | iscount: int | None = Field(None, description="是否计数") 59 | labelList: str | None = Field(None, description="标签") 60 | level: str | None = Field(None, description="等级") 61 | nickname: str | None = Field(None, description="昵称") 62 | openid: str | None = Field(None, description="openid") 63 | parentService: str | None = Field(None, description="父级服务") 64 | preService: str | None = Field(None, description="前级服务") 65 | privilege: str | None = Field(None, description="权限") 66 | province: str | None = Field(None, description="省份") 67 | score: int | None = Field(None, description="积分") 68 | sex: str | None = Field(None, description="性别") 69 | unionid: str | None = Field(None, description="unionid") 70 | updateTime: str | None = Field(None, description="更新时间") 71 | updateUser: str | None = Field(None, description="更新人") 72 | userId: str | None = Field(None, description="用户ID") 73 | userLabel: str | None = Field(None, description="用户标签") 74 | userShowId: int | None = Field(None, description="用户ID") 75 | wdPassword: str | None = Field(None, description="微信密码") 76 | 77 | def __str__(self): 78 | return "\n".join([ 79 | "登录成功,信息如下", 80 | f"❄️>> 上级ID: {self.accountId}", 81 | f"❄️>> 用户ID: {self.userShowId}", 82 | f"❄️>> 用户昵称: {self.nickname}", 83 | f"❄️>> 是否有密码: {self.isPwd}", 84 | f"❄️>> 创建日期: {self.createTime}", 85 | ]) 86 | 87 | 88 | class RspLogin(CommonRsp): 89 | data: RspLoginInfo 90 | 91 | def __str__(self): 92 | return self.data.__str__() 93 | 94 | def __repr__(self): 95 | return self.__str__() 96 | 97 | 98 | class RspUserinfoData(BaseModel): 99 | activityScore: int | None = Field(None, description="文章阅读积分一轮可得") 100 | alreadyMoney: int | None = Field(None, description="已提金额") 101 | cashMoney: float | None = Field(None, description="当前余额") 102 | cashScore: int | None = Field(None, description="当前积分") 103 | cheatScore: int | None = Field(None, description="作弊积分(不知是否是这个意思)") 104 | consumeScore: int | None = Field(None, description="消费积分") 105 | drawScore: int | None = Field(None, description="提现积分") 106 | extendAwardScore: int | None = Field(None, description="推广奖励积分") 107 | extendScore: int | None = Field(None, description="推广积分") 108 | masterQQCrowd: str | None = Field(None, description="大佬群") 109 | otherScore: int | None = Field(None, description="其他积分") 110 | sumScore: int | None = Field(None, description="总积分") 111 | userShowId: int | None = Field(None, description="用户ID") 112 | 113 | def __str__(self): 114 | return "\n".join([ 115 | "用户信息", 116 | f"❄️>> 用户ID: {self.userShowId}", 117 | f"❄️>> 当前余额: {self.cashMoney}", 118 | f"❄️>> 当前积分: {self.cashScore}", 119 | f"❄️>> 推广积分: {self.extendScore}", 120 | f"❄️>> 总得积分: {self.sumScore}", 121 | ]) 122 | 123 | def __repr__(self): 124 | return self.__str__() 125 | 126 | 127 | class RspUserInfo(CommonRsp): 128 | data: RspUserinfoData 129 | 130 | def __str__(self): 131 | return self.data.__str__() 132 | 133 | def __repr__(self): 134 | return self.__str__() 135 | 136 | 137 | class RspTaskInfo(BaseModel): 138 | btnName: str | None = Field(None, description="按钮名") 139 | countDown: int | None = Field(None, description="倒计时") 140 | createTime: str | None = Field(None, description="创建时间") 141 | createUser: int | None = Field(None, description="") 142 | dayUpperLimit: int | None = Field(None, description="每日上限") 143 | ico: str | None = Field(None, description="图标名称和后缀名") 144 | id: int | None = Field(None, description="任务ID") 145 | isShowBtn: int | None = Field(None, description="是否显示按钮") 146 | perMsg: str | None = Field(None, description="展示消息") 147 | perUpperLimit: int | None = Field(None, description="每次上限") 148 | remark: str | None = Field(None, description="备注") 149 | score: int | None = Field(None, description="积分") 150 | scoreLow: int | None = Field(None, description="积分下限") 151 | scoreMsg: str | None = Field(None, description="积分说明") 152 | scoreTall: int | None = Field(None, description="积分上限") 153 | sort: int | None = Field(None, description="排序") 154 | timeLimit: int | None = Field(None, description="时间限制") 155 | titleMsg: str | None = Field(None, description="标题说明") 156 | toAccount: str | None = Field(None, description="展示消息") 157 | type: int | None = Field(None, description="任务类型") 158 | typeName: str | None = Field(None, description="任务类型名称") 159 | typeStatus: int | None = Field(None, description="任务状态") 160 | updateTime: str | None = Field(None, description="更新时间") 161 | updateUser: str | None = Field(None, description="更新用户") 162 | 163 | 164 | class RspTaskList(CommonRsp): 165 | """任务列表响应数据""" 166 | data: List[RspTaskInfo] | None = Field(None, description="任务列表") 167 | 168 | 169 | class RspArticleData(BaseModel): 170 | """""" 171 | code: str | None = Field(None, description="响应码") 172 | startNum: str | None = Field(None, description="当前阅读数") 173 | putid: str | None = Field(None, description="ID") 174 | endNum: str | None = Field(None, description="最大结束的阅读数") 175 | url: str | None = Field(None, description="文章链接") 176 | 177 | 178 | class RspArticleUrl(CommonRsp): 179 | """文章列表响应数据""" 180 | data: RspArticleData | None = Field(None, description="阅读文章链接信息") 181 | 182 | 183 | class RspSignInData(BaseModel): 184 | code: int | None = Field(None, description="响应码") 185 | today: int | None = Field(None, description="今日签到,获得了多少积分") 186 | tomorrow: int | None = Field(None, description="明日签到,获得了多少积分") 187 | day: int | None = Field(None, description="今天是第几天签到") 188 | 189 | def __str__(self): 190 | return "\n".join([ 191 | "签到成功,信息如下", 192 | f"❄️>> 今日签到积分: {self.today}", 193 | f"❄️>> 明日签到积分: {self.tomorrow}", 194 | f"❄️>> 今天是第{self.day}天签到", 195 | ]) 196 | 197 | def __repr__(self): 198 | return self.__str__() 199 | 200 | 201 | class RspSignIn(CommonRsp): 202 | """签到响应数据""" 203 | data: RspSignInData | None = Field(None, description="签到信息") 204 | 205 | def __str__(self): 206 | return self.data.__str__() 207 | 208 | def __repr__(self): 209 | return self.__str__() 210 | 211 | 212 | class RspWithdrawOptionData(BaseModel): 213 | id: str | None = Field(None, description="ID") 214 | money: int | None = Field(None, description="提现金额") 215 | moneyType: int | None = Field(None, description="提现类型") 216 | onMoney: bool | None = Field(None, description="是否开启提现") 217 | status: int | None = Field(None, description="状态") 218 | 219 | 220 | class RspWithdrawOptions(CommonRsp): 221 | data: List[RspWithdrawOptionData] | None = Field(None, description="提现选项列表") 222 | -------------------------------------------------------------------------------- /config/biz_data.yaml: -------------------------------------------------------------------------------- 1 | - Mzg2Mzg3MDc2Mg== 2 | - MzI3Mjk1OTAzMA== 3 | - MzIxNDIzNDI4Mg== 4 | - MzUzODY4NzE2OQ== 5 | - MzA3NDA2NDQxMw== 6 | - Mzg4NTcwODE1NA== 7 | - MzU2MTAyNzg0Mw== 8 | - MzUyOTcxNTQzOQ== 9 | - MjM5ODQ4OTA4Mg== 10 | - MzU1OTI5Njg5Nw== 11 | - Mzg2NDA5NDQ0Nw== 12 | - MzIyOTcwNjE3OA== 13 | - MzI1NjE3NjQ0Ng== 14 | - MzIzNzI2NTk2Mg== 15 | - MzkxMzMwNTkxOA== 16 | - MjM5ODM0OTcwMQ== 17 | - MzA4MDMwNjcxNg== 18 | - MzU4MTAwMzE1NA== 19 | - MzIwMzQ2MzY5Mw== 20 | - MzA4ODE1NDUwNw== 21 | - MzI2MDI3MDA5OQ== 22 | - MzA5MTYxNzkxMw== 23 | - MzI2NDk5NzA0Mw== 24 | - Mzg3NzUxMjc5Mg== 25 | - MzIyMDc1MzAzMg== 26 | - MzI0ODA0NTY3OA== 27 | - MzkzNTUwODExMQ== 28 | - MzUzNTkyODAzOQ== 29 | - MjM5MTExMTMwOQ== 30 | - MzU4OTcyMDYzNg== 31 | - MzA4MjU5NTEwMQ== 32 | - MzA5MTUxNDAxNA== 33 | - MzA3MTk3MTI0NA== 34 | - MzI0MjU2NTA1Mg== 35 | - Mzg5NTUzODY4NA== 36 | - MzA4MjQ5MzIxNQ== 37 | - MzA3NDY5NzkwMQ== 38 | - MzAxMzAyNzg3Ng== 39 | - Mzg3Njg2MzM2OQ== 40 | - MzA4NjMwMzQyNw== 41 | - MzkyODQ5NDMyMw== 42 | - MzI0MTAyNzcxNg== 43 | - MjM5NzI3Njc2MA== 44 | - MzkzMzE3NDU1NA== 45 | - MjM5NTkyNzUzMg== 46 | - MzI2MjI0OTY3NQ== 47 | - MzA4MjgzNDMzNw== 48 | - MjM5NjA5NzA0Ng== 49 | - Mzg3MTg2MDM2NQ== 50 | - MzA4MjQxNjQzMA== 51 | - MzA4MDIyMDk0Ng== 52 | - MjM5MDkyMzU5MQ== 53 | - MzkzNzU5OTU4NQ== 54 | - MzU0Njk1MjkyMA== 55 | - MjM5MjMzODUyMA== 56 | - MjM5MTA2MzY4NA== 57 | - MjM5OTMxMzEzNQ== 58 | - MjM5NzEyMjgyNQ== 59 | - MzI4MDI5MzIzOA== 60 | - MzA3MTA4MTEwNg== 61 | - MzAxNDIxNTYwMQ== 62 | - MzU2MTU4MTc1Ng== 63 | - Mzg2MTQzNjY0MQ== 64 | - MzkxNDYzOTEyMw== 65 | - MzkxMjQ0MTUyNw== 66 | - MzUzNDY0NzQwMQ== 67 | - MjM5MTA5MDM3NA== 68 | - Mzg3MjAyNzE2MA== 69 | - Mzg4ODc2MDk0NQ== 70 | - Mzg5ODUyMzYzMQ== 71 | - MjM5MjU2NjEwMA== 72 | - MjM5NDA4MzEwMA== 73 | - MjM5MDA0ODQyMA== 74 | - MzkxMzQzMDI3OQ== 75 | - MzA4OTYzMTE0OQ== 76 | - MzUzOTExMTYxMw== 77 | - MjM5NTIzNjA2MA== 78 | - MzkwNTY1MzYxOQ== 79 | - Mzg5NjczMzgxMA== 80 | - Mzg3MTc5MTEwMQ== 81 | - MjM5NjA2OTg0MQ== 82 | - MzIyNjA0MjQ1OQ== 83 | - MzI0NDYwNjMzMg== 84 | - MzA3OTYyNzgyNw== 85 | - MzA5MzAxNjE5NA== 86 | - MzU4MjY0NjQ0MA== 87 | - MzAwMjI0MzAwNA== 88 | - MjM5MDE2MzIxMQ== 89 | - MzAwMjgwOTkyNQ== 90 | - MzIyODg2NTkwMA== 91 | - MzU3NjA1OTgxNg== 92 | - MjM5MzA5NTMwMA== 93 | - MzA4OTQ3NTc0Nw== 94 | - MjM5OTcxMzE3Ng== 95 | - MzI0NzE1MjE2OA== 96 | - MzA3MjY5MzUxNg== 97 | - MzkwODM5NjU5MA== 98 | - Mzk0ODI1NTc2NQ== 99 | - MzA4MjA2NzU2OQ== 100 | - MzU2MjQwNDI3OA== 101 | - MzU5NDQxNjM4OA== 102 | - MzkxNzI2MTY2MQ== 103 | - MzU1NjY3MDA3NA== 104 | - MzU4NDc3MTEwMw== 105 | - MjM5MjQ3NDcyMA== 106 | - Mzg3NDg5ODM1NA== 107 | - MzA5NjUwMDMzNg== 108 | - MzU5NDM5OTY2OA== 109 | - MzIwMjA4MDUyNA== 110 | - MzAxMzQ4MDE1Mg== 111 | - MzI1MDQ5ODk5OQ== 112 | - MzI4MDQzNTY0OA== 113 | - MjM5Mzg3NjMxNA== 114 | - MzI5Mzg5NTM1Nw== 115 | - MzIyNjQ3NDM1Mw== 116 | - Mzg5OTY2NDYzMQ== 117 | - MzkyMjYxNzQ2NA== 118 | - Mzg4NDYwNTAxNA== 119 | - MzkyMjE3MzYxMg== 120 | - Mzg5MTYzMjg2MQ== 121 | - MzkwNzUzODAxOQ== 122 | - MzIyMTE2NTExOA== 123 | - MjM5MjA0MTk4MA== 124 | - MzU4OTc3ODg1NQ== 125 | - MjM5NTI1OTY1Mg== 126 | - MzkyNjY0MTExOA== 127 | - MzAxNTM0MDk1MA== 128 | - MjM5NDAzNDIyMA== 129 | - MzA5OTg0MDEyMA== 130 | - Mzg3NTg2NDE5Ng== 131 | - MzU5NzE0MDM1Mg== 132 | - MzAxMzExNDU0Mg== 133 | - Mzg5MDgxODAzMg== 134 | - Mzg3MzgzOTA4OA== 135 | - MzkwMTYwNzcwMw== 136 | - MzkyMDYyMDMzMg== 137 | - Mzg2NjUyMjI1NA== 138 | - MzI4MjQ3NDM1Nw== 139 | - MzU5ODgwNDk4Nw== 140 | - MzI1MjU0ODUxNg== 141 | - MzkzNDYxODY5OA== 142 | - MzA3MjU2MjgwNw== 143 | - MzkyMzIyNDEwOQ== 144 | - MzU0ODg5OTkwMQ== 145 | - Mzg5OTkxNjUxMA== 146 | - Mzg5OTg2MjQ5NQ== 147 | - MzAwNzU0NTM3NA== 148 | - MzIwOTQwMDczMw== 149 | - MzkwNzYwNDYyMQ== 150 | - MzIyMjc0NzU4OA== 151 | - MzkxNTE3MzQ4MQ== 152 | - MzkxNTE0MTg2Mw== 153 | - MzU5MzA1MTI3OQ== 154 | - MzUyMzMyODU4OA== 155 | - MzIzNzI2NzEyMg== 156 | - MjM5MTM3NTMwNA== 157 | - Mzg4OTc2NDgxNw== 158 | - MzU0NzI5Mjc4OQ== 159 | - MzA4NzAzNDIxMw== 160 | - MjM5MjE1OTk1OA== 161 | - Mzg2MTY1MTk2Mg== 162 | - MzA4NTU2ODEwNw== 163 | - Mzg2NTg4NDk3Mg== 164 | - MzU3MTA1NTMwMA== 165 | - MjM5MTM1NTA2MA== 166 | - MzI0NzQ2MDA4OQ== 167 | - MzI5NjQyNjE3OQ== 168 | - MzU3MjkxNzYzOQ== 169 | - MzI2MzYxMDEyMg== 170 | - Mzg2NTMzODU0Mg== 171 | - MzA5MzM4Mjc3OA== 172 | - MjM5MTA0NTUwNw== 173 | - Mzg3MDQ3MTE1Nw== 174 | - MzAxMjY4ODg3MQ== 175 | - MzU4NjEyODg3Ng== 176 | - MzI0MDA0NjUyMw== 177 | - MzkwMzYyMTExMQ== 178 | - MzA3MTk4MTYyMA== 179 | - MzIxNzM4Njc0Mw== 180 | - MzA4MDMzNjM4OA== 181 | - MzA4NzU5MjQxMA== 182 | - MzIxODA2ODQxOA== 183 | - MzI3NjI0NzE5Mw== 184 | - MzIzMDczODg4Mw== 185 | - MzkxNjU3NzQ3MA== 186 | - MzI4MTU0MDEzNQ== 187 | - MjM5NDYzNDkwMA== 188 | - MzU1NzcwOTY5NA== 189 | - MzI3OTg3MzEwMA== 190 | - MzI3MDE2OTc1MQ== 191 | - MzI2OTE2NTI2MQ== 192 | - MzU5MzcxMDQ4Mg== 193 | - MzA4NTA5NzQyNg== 194 | - MzA4NzI4NTUzMg== 195 | - MjM5NjE4NDk0Mg== 196 | - MzAwOTI5NTUyMg== 197 | - MzA3MzcyNjQxMg== 198 | - MzkwNDIxNzM2MA== 199 | - MzA3NTQ5NDIxMg== 200 | - Mzg2MjYzNTkyMA== 201 | - MzA5NDEzODUwNQ== 202 | - MzIwOTgzOTAyOA== 203 | - Mzg3NzcyNTc0Nw== 204 | - MzI4MTQyNTQ5Mg== 205 | - MzA4MTQ4NjQzMw== 206 | - MzA3NTc4MjMwMA== 207 | - MzI3MzE2NzE5NQ== 208 | - MzA3OTY5NTUwNQ== 209 | - MzkxNjIzOTYwMQ== 210 | - MzkzNzI3NTc1Mg== 211 | - MzA4OTI2NjIzOA== 212 | - MjM5NDIxMzkyMQ== 213 | - MzA4NDg1NjYyNQ== 214 | - MzI3MDE3Nzg4NQ== 215 | - MzkyMDUxNDQxNw== 216 | - MzAwOTU3NzQ2Mg== 217 | - MjM5MzMxNjUxNQ== 218 | - MzU2MzcyNzgzOQ== 219 | - MzUzNzE2NTc1NA== 220 | - MjM5NDE1NDc0OQ== 221 | - MzIyNTY0OTQyOQ== 222 | - MzI0ODMzNDkzMQ== 223 | - MjM5NTEyMDIyMg== 224 | - MzI4NjU0MDA5Mg== 225 | - MzU5Mjg1MjA3Mw== 226 | - MzkyODE5NzEzMA== 227 | - MjM5NTU0MjM0MA== 228 | - MzI3ODUxODIxNQ== 229 | - MzU4MTI1MTAyOQ== 230 | - MzA4Nzg4NTU3OA== 231 | - MzIxMDAwNzg2Mw== 232 | - MzI2MzU4ODQxMg== 233 | - MjM5MDAwMzk4Mw== 234 | - MzA3MTI0ODQyNg== 235 | - MzkzODUxNTM2OA== 236 | - MzU3MzU0NjgwNQ== 237 | - MjM5NjE2NzM0MA== 238 | - MzUxMTcwNzk0MA== 239 | - MzA5NTk3NjgyMg== 240 | - MzIyMTY5MDMxMg== 241 | - MzI2OTI0NTY3Nw== 242 | - MzU3NTUzNDQxMg== 243 | - MzkxODUwODgwMQ== 244 | - MzU3ODcwMDU2Mw== 245 | - MzA3NTc0ODY4MA== 246 | - MzI2OTM4NzYzMA== 247 | - MjM5NDg0Mzc5NA== 248 | - MzAwNzEyMTg4Mg== 249 | - MzIxNjM4NzkwMA== 250 | - MjM5MzkxODMzMw== 251 | - Mzk0NjQzODIwNQ== 252 | - MzIxMjk0MzI3OA== 253 | - MzA4NjAzNzkxNA== 254 | - MzI5OTE4NDc3Mw== 255 | - MjM5OTUyOTUyMA== 256 | - MzA3MzE1NjI3MA== 257 | - Mzk0MjQwMTg2NA== 258 | - MzkxNDQzOTE4MA== 259 | - MzU0MDg2OTQwMA== 260 | - MzU0OTE3MzQ0Mg== 261 | - MzIyMDcwMjAwMA== 262 | - MjM5MDA2Mjk2MA== 263 | - MjM5NTU5MzMwMA== 264 | - MzA5NTkyMzIzMQ== 265 | - MzI3NTg4NDkwNg== 266 | - Mzg2NzY2MTc1Mw== 267 | - MjM5MjY1NjgwMQ== 268 | - MzkwMzUwNzMzNQ== 269 | - MzI1MjkzMjUzMA== 270 | - MzIwOTQ3MjY1Ng== 271 | - MzIwNjQ0Mzg4OQ== 272 | - MjM5MTM1NzQ3Mg== 273 | - MzI0MjY0OTE0MQ== 274 | - Mzg4OTg2MDM3MA== 275 | - MzAwNjI1MzI0OQ== 276 | - Mzg3MDU1MDY1NQ== 277 | - MzU4NjcwNzA0OA== 278 | - MzA5ODIxOTg0Mw== 279 | - MjM5NTE0NDczNQ== 280 | - Mzg3NDc2NTkzNw== 281 | - MzI5MjY5MDEyNQ== 282 | - MzA5MTU4ODQ0OQ== 283 | - MzA4ODY2NDMyMA== 284 | - MzUxNDE3NDYyNw== 285 | - MzI4MTk2MjczNA== 286 | - MjM5MTY0Njc2MQ== 287 | - MzIzMjc0MjQ2OA== 288 | - MzkyMTY2MzcyOA== 289 | - Mzg5NzU4NTAwNw== 290 | - MzUyNTA2MTM0OA== 291 | - MjM5MDg3MDYwMQ== 292 | - MzUyNzk3Mzg0NQ== 293 | - Mzg2OTY5ODQ3MA== 294 | - MzkyMDMzNTQ3MQ== 295 | - MzkwNjI3MDgxMQ== 296 | - Mzg5MjM0MDEwNw== 297 | - MzkxNDU1NDEzNw== 298 | - MzAwNzM2NjY1Mg== 299 | - MzkzNTYxOTgyMA== 300 | - Mzk0ODIxODE4OQ== 301 | - MjM5NTQ5NzQyMg== 302 | - MzIzNjc1NzUzMw== 303 | - MzAwNzIwNzIyMg== 304 | - MzIzOTUyODk3MA== 305 | - MzI2NzYxNzkzNw== 306 | - MzU2MzA1MTM3Mg== 307 | - MzIwOTcwNjM2OA== 308 | - MzA3MjAxNzQzNw== 309 | - MzA4ODU3MTUzNw== 310 | - MzIxNDA5OTgyNA== 311 | - MjM5NDM0ODEyMA== 312 | - MjM5OTY1MDMxMg== 313 | - MzAwMzExNjU2Nw== 314 | - MzkzMTYyMDU0OQ== 315 | - MzkxMTQ0NTgxMQ== 316 | - MzA3NTg5NjYwNA== 317 | - MzI3ODA5MzgyMw== 318 | - Mzg3NTYxMjU5Mw== 319 | - MzAxOTMyNTY4OQ== 320 | - MzU2NDY0NjkwNg== 321 | - MzAxODUxNDcwNQ== 322 | - MzA5NjkxNzEwMQ== 323 | - MzkxMDY1Mjk5MQ== 324 | - MzA3NjIxODQxOA== 325 | - Mzg3ODcwMTQxMg== 326 | - MjM5NTkwNTI2MQ== 327 | - MzIzNjMyMjkzOA== 328 | - MzI5NTM1ODQzOQ== 329 | - Mzg5MDA4ODg1NQ== 330 | - Mzg5OTYzODE4Mg== 331 | - MzkxNjMwNDIzOA== 332 | - MjM5MjEzNzYzOA== 333 | - MzI3MDY1NzIzMA== 334 | - MzA5ODYzNzIzMA== 335 | - MzA4MDE4NTUyNg== 336 | - MzI1MzA0MjkzNQ== 337 | - Mzg5NTg3NTM5Mw== 338 | - MzU2ODI5ODMzNg== 339 | - MzA5NDA3MjMxNQ== 340 | - MzU4NzczNzYyNg== 341 | - MzAwMjc1ODA1MA== 342 | - MjM5MTE0MjAxNQ== 343 | - Mzg3NjI5NjAxNw== 344 | - MzA4Njk4NTQwOQ== 345 | - MzIyNjI3MjgwMQ== 346 | - MzIyMTUyNTk1Ng== 347 | - MjM5MTE2NDMyMQ== 348 | - MzIzMzUyMjQ4NA== 349 | - MzI2MDI4NjQ3Mw== 350 | - MzkzNDM4MzkyNQ== 351 | - MjM5MjY2MzQ0MA== 352 | - MzIwMzEwMTgzMw== 353 | - MzI2MDAzNzkzNA== 354 | - MzUyNDkyMTQ1Ng== 355 | - MzkxNDMyOTk1NQ== 356 | - MzkzNjQzMzk5Mw== 357 | - Mzg3MzY2NDE0MQ== 358 | - MzkxMjM5NDY0OQ== 359 | - MjM5MDgwNDI0MA== 360 | - MzA5MjA3Nzk2Ng== 361 | - MzI4MDQzNDc1OQ== 362 | - MzI3NDU0NDkyNQ== 363 | - Mzg3MTk2ODgzOQ== 364 | - MjM5MDg5MjgwMg== 365 | - MzUxOTI5NDgwMQ== 366 | - MjM5NDgyNjc0MA== 367 | - MjM5NzAwMjk4Mw== 368 | - MzU5MjI5NzkxNQ== 369 | - Mzg4Njk0NzE2Nw== 370 | - MjM5MjU5MDAzNg== 371 | - MzU5NzEwNjA1NA== 372 | - MzA4NTI2ODc5Ng== 373 | - MzIwODAxMzA1OA== 374 | - MzIxNTczNDU4NQ== 375 | - MzkzOTYwMjE2NA== 376 | - MzAxMTUyNzQ0Ng== 377 | - MzIxODY4MDg1OA== 378 | - MjM5MDI3MzU3Mg== 379 | - MzU4ODE2NjYwMQ== 380 | - MzU3MDM0MzkxOA== 381 | - MjM5NjIyNDAwMA== 382 | - MzI1MzUyMDQ3NA== 383 | - MzU0OTA1NTUzNQ== 384 | - MzI5MzE1Mzk1OQ== 385 | - MTY0MzI5NDcwMQ== 386 | - MzI0NTAwNDg4OQ== 387 | - MzAxMDEwMDA3Nw== 388 | - MzI2MTYzNjg2Ng== 389 | - MzkzNjUxOTg5OQ== 390 | - Mzk0NjQ5ODU2OQ== 391 | - MjM5OTc4MDY2OQ== 392 | - MjM5MzEzMTA2MA== 393 | - MzkxNDI4NDg0Mg== 394 | - Mzg5OTkwMzM0Ng== 395 | - Mzk0MDMyMTMzNg== 396 | - MjM5MzQ5MDA4MA== 397 | - MzUyMDMxNTkxMA== 398 | - MzA4OTU5NjYwNw== 399 | - Mzg5NTA0MTQwMw== 400 | - MjM5Njg5MTU5Mg== 401 | -------------------------------------------------------------------------------- /app/api/callback/wx_bussiness/WXBizMsgCrypt3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding:utf-8 -*- 3 | 4 | """ 对企业微信发送给企业后台的消息加解密示例代码. 5 | @copyright: Copyright (c) 1998-2014 Tencent Inc. 6 | 7 | """ 8 | # ------------------------------------------------------------------------ 9 | import logging 10 | import base64 11 | import random 12 | import hashlib 13 | import time 14 | import struct 15 | from Crypto.Cipher import AES 16 | import xml.etree.cElementTree as ET 17 | import socket 18 | 19 | from . import ierror 20 | 21 | """ 22 | 关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案 23 | 请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。 24 | 下载后,按照README中的“Installation”小节的提示进行pycrypto安装。 25 | """ 26 | 27 | 28 | class FormatException(Exception): 29 | pass 30 | 31 | 32 | def throw_exception(message, exception_class=FormatException): 33 | """my define raise exception function""" 34 | raise exception_class(message) 35 | 36 | 37 | class SHA1: 38 | """计算企业微信的消息签名接口""" 39 | 40 | def getSHA1(self, token, timestamp, nonce, encrypt): 41 | """用SHA1算法生成安全签名 42 | @param token: 票据 43 | @param timestamp: 时间戳 44 | @param encrypt: 密文 45 | @param nonce: 随机字符串 46 | @return: 安全签名 47 | """ 48 | try: 49 | sortlist = [token, timestamp, nonce, encrypt] 50 | sortlist.sort() 51 | sha = hashlib.sha1() 52 | sha.update("".join(sortlist).encode('utf-8')) 53 | return ierror.WXBizMsgCrypt_OK, sha.hexdigest() 54 | except Exception as e: 55 | logger = logging.getLogger() 56 | logger.error(e) 57 | return ierror.WXBizMsgCrypt_ComputeSignature_Error, None 58 | 59 | 60 | class XMLParse: 61 | """提供提取消息格式中的密文及生成回复消息格式的接口""" 62 | 63 | # xml消息模板 64 | AES_TEXT_RESPONSE_TEMPLATE = """ 65 | 66 | 67 | %(timestamp)s 68 | 69 | """ 70 | 71 | def extract(self, xmltext): 72 | """提取出xml数据包中的加密消息 73 | @param xmltext: 待提取的xml字符串 74 | @return: 提取出的加密消息字符串 75 | """ 76 | try: 77 | xml_tree = ET.fromstring(xmltext) 78 | encrypt = xml_tree.find("Encrypt") 79 | return ierror.WXBizMsgCrypt_OK, encrypt.text 80 | except Exception as e: 81 | logger = logging.getLogger() 82 | logger.error(e) 83 | return ierror.WXBizMsgCrypt_ParseXml_Error, None 84 | 85 | def generate(self, encrypt, signature, timestamp, nonce): 86 | """生成xml消息 87 | @param encrypt: 加密后的消息密文 88 | @param signature: 安全签名 89 | @param timestamp: 时间戳 90 | @param nonce: 随机字符串 91 | @return: 生成的xml字符串 92 | """ 93 | resp_dict = { 94 | 'msg_encrypt': encrypt, 95 | 'msg_signaturet': signature, 96 | 'timestamp': timestamp, 97 | 'nonce': nonce, 98 | } 99 | resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict 100 | return resp_xml 101 | 102 | 103 | class PKCS7Encoder(): 104 | """提供基于PKCS7算法的加解密接口""" 105 | 106 | block_size = 32 107 | 108 | def encode(self, text): 109 | """ 对需要加密的明文进行填充补位 110 | @param text: 需要进行填充补位操作的明文 111 | @return: 补齐明文字符串 112 | """ 113 | text_length = len(text) 114 | # 计算需要填充的位数 115 | amount_to_pad = self.block_size - (text_length % self.block_size) 116 | if amount_to_pad == 0: 117 | amount_to_pad = self.block_size 118 | # 获得补位所用的字符 119 | pad = chr(amount_to_pad) 120 | return text + (pad * amount_to_pad).encode() 121 | 122 | def decode(self, decrypted): 123 | """删除解密后明文的补位字符 124 | @param decrypted: 解密后的明文 125 | @return: 删除补位字符后的明文 126 | """ 127 | # pad = ord(decrypted[-1]) 128 | pad = decrypted[-1] 129 | if pad < 1 or pad > 32: 130 | pad = 0 131 | return decrypted[:-pad] 132 | 133 | 134 | class Prpcrypt(object): 135 | """提供接收和推送给企业微信消息的加解密接口""" 136 | 137 | def __init__(self, key): 138 | 139 | # self.key = base64.b64decode(key+"=") 140 | self.key = key 141 | # 设置加解密模式为AES的CBC模式 142 | self.mode = AES.MODE_CBC 143 | 144 | def encrypt(self, text, receiveid): 145 | """对明文进行加密 146 | @param text: 需要加密的明文 147 | @return: 加密得到的字符串 148 | """ 149 | # 16位随机字符串添加到明文开头 150 | text = text.encode() 151 | text = self.get_random_str() + struct.pack("I", socket.htonl(len(text))) + text + receiveid.encode() 152 | 153 | # 使用自定义的填充方式对明文进行补位填充 154 | pkcs7 = PKCS7Encoder() 155 | text = pkcs7.encode(text) 156 | # 加密 157 | cryptor = AES.new(self.key, self.mode, self.key[:16]) 158 | try: 159 | ciphertext = cryptor.encrypt(text) 160 | # 使用BASE64对加密后的字符串进行编码 161 | return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext) 162 | except Exception as e: 163 | logger = logging.getLogger() 164 | logger.error(e) 165 | return ierror.WXBizMsgCrypt_EncryptAES_Error, None 166 | 167 | def decrypt(self, text, receiveid): 168 | """对解密后的明文进行补位删除 169 | @param text: 密文 170 | @return: 删除填充补位后的明文 171 | """ 172 | try: 173 | cryptor = AES.new(self.key, self.mode, self.key[:16]) 174 | # 使用BASE64对密文进行解码,然后AES-CBC解密 175 | plain_text = cryptor.decrypt(base64.b64decode(text)) 176 | except Exception as e: 177 | logger = logging.getLogger() 178 | logger.error(e) 179 | return ierror.WXBizMsgCrypt_DecryptAES_Error, None 180 | try: 181 | pad = plain_text[-1] 182 | # 去掉补位字符串 183 | # pkcs7 = PKCS7Encoder() 184 | # plain_text = pkcs7.encode(plain_text) 185 | # 去除16位随机字符串 186 | content = plain_text[16:-pad] 187 | xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0]) 188 | xml_content = content[4: xml_len + 4] 189 | from_receiveid = content[xml_len + 4:] 190 | except Exception as e: 191 | logger = logging.getLogger() 192 | logger.error(e) 193 | return ierror.WXBizMsgCrypt_IllegalBuffer, None 194 | 195 | if from_receiveid.decode('utf8') != receiveid: 196 | return ierror.WXBizMsgCrypt_ValidateCorpid_Error, None 197 | return 0, xml_content 198 | 199 | def get_random_str(self): 200 | """ 随机生成16位字符串 201 | @return: 16位字符串 202 | """ 203 | return str(random.randint(1000000000000000, 9999999999999999)).encode() 204 | 205 | 206 | class WXBizMsgCrypt(object): 207 | # 构造函数 208 | def __init__(self, sToken, sEncodingAESKey, sReceiveId): 209 | try: 210 | self.key = base64.b64decode(sEncodingAESKey + "=") 211 | assert len(self.key) == 32 212 | except: 213 | throw_exception("[error]: EncodingAESKey unvalid !", FormatException) 214 | # return ierror.WXBizMsgCrypt_IllegalAesKey,None 215 | self.m_sToken = sToken 216 | self.m_sReceiveId = sReceiveId 217 | 218 | # 验证URL 219 | # @param sMsgSignature: 签名串,对应URL参数的msg_signature 220 | # @param sTimeStamp: 时间戳,对应URL参数的timestamp 221 | # @param sNonce: 随机串,对应URL参数的nonce 222 | # @param sEchoStr: 随机串,对应URL参数的echostr 223 | # @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效 224 | # @return:成功0,失败返回对应的错误码 225 | 226 | def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr): 227 | sha1 = SHA1() 228 | ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr) 229 | if ret != 0: 230 | return ret, None 231 | if not signature == sMsgSignature: 232 | return ierror.WXBizMsgCrypt_ValidateSignature_Error, None 233 | pc = Prpcrypt(self.key) 234 | ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sReceiveId) 235 | return ret, sReplyEchoStr 236 | 237 | def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None): 238 | # 将企业回复用户的消息加密打包 239 | # @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串 240 | # @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间 241 | # @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce 242 | # sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串, 243 | # return:成功0,sEncryptMsg,失败返回对应的错误码None 244 | pc = Prpcrypt(self.key) 245 | ret, encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId) 246 | encrypt = encrypt.decode('utf8') 247 | if ret != 0: 248 | return ret, None 249 | if timestamp is None: 250 | timestamp = str(int(time.time())) 251 | # 生成安全签名 252 | sha1 = SHA1() 253 | ret, signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt) 254 | if ret != 0: 255 | return ret, None 256 | xmlParse = XMLParse() 257 | return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce) 258 | 259 | def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce): 260 | # 检验消息的真实性,并且获取解密后的明文 261 | # @param sMsgSignature: 签名串,对应URL参数的msg_signature 262 | # @param sTimeStamp: 时间戳,对应URL参数的timestamp 263 | # @param sNonce: 随机串,对应URL参数的nonce 264 | # @param sPostData: 密文,对应POST请求的数据 265 | # xml_content: 解密后的原文,当return返回0时有效 266 | # @return: 成功0,失败返回对应的错误码 267 | # 验证安全签名 268 | xmlParse = XMLParse() 269 | ret, encrypt = xmlParse.extract(sPostData) 270 | if ret != 0: 271 | return ret, None 272 | sha1 = SHA1() 273 | ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt) 274 | if ret != 0: 275 | return ret, None 276 | if not signature == sMsgSignature: 277 | return ierror.WXBizMsgCrypt_ValidateSignature_Error, None 278 | pc = Prpcrypt(self.key) 279 | ret, xml_content = pc.decrypt(encrypt, self.m_sReceiveId) 280 | return ret, xml_content 281 | -------------------------------------------------------------------------------- /schema/klyd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # klyd.py created by MoMingLog on 30/3/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-03-30 6 | 【功能描述】 7 | """ 8 | import re 9 | from typing import Type, Dict 10 | 11 | from httpx import URL 12 | from pydantic import BaseModel, create_model, Field, HttpUrl, validator 13 | 14 | from schema.common import CommonGlobalConfig, CommonPartConfig 15 | 16 | __all__ = [ 17 | "KLYDAccount", 18 | "KLYDConfig", 19 | "RspRecommend" 20 | ] 21 | 22 | from utils.logger_utils import NestedLogColors 23 | 24 | 25 | class CommonKLYDConfig(BaseModel): 26 | """可乐阅读全局和局部的相同配置""" 27 | withdraw_type: str = Field(None, description="提现类型: wx 微信, ali 支付宝") 28 | just_in_case: bool | None = Field(None, description="以防万一开关") 29 | unknown_to_push: bool | None = Field(None, description="未知走推送开关") 30 | 31 | 32 | class KLYDAccount(CommonPartConfig, CommonKLYDConfig): 33 | """账号配置(局部配置)""" 34 | cookie: str 35 | 36 | 37 | class BaseKLYDGlobalConfig(CommonGlobalConfig, CommonKLYDConfig): 38 | """可乐阅读配置(全局配置)""" 39 | biz_data: list | None = Field(None, description="检测文章的biz") 40 | 41 | 42 | KLYDConfig: Type[BaseKLYDGlobalConfig] = create_model( 43 | 'KLYDConfig', 44 | account_data=(Dict[str | int, KLYDAccount], {}), 45 | source=(str, "klyd.yaml"), 46 | __base__=BaseKLYDGlobalConfig 47 | ) 48 | 49 | 50 | class RspRecommendUser(BaseModel): 51 | """推荐 响应数据 User""" 52 | username: str = Field(..., description="用户名") 53 | upuid: str = Field(..., description="上级用户uid") 54 | uid: str = Field(..., description="当前用户uid") 55 | regtime: str = Field(..., description="注册日期") 56 | score: float = Field(..., description="当前积分") 57 | rebate_count_show: bool 58 | rebate_count: str 59 | new_read_count: str = Field(..., description="新阅读数") 60 | 61 | def __str__(self): 62 | msg_list = [ 63 | f"{NestedLogColors.black('【用户信息】')}", 64 | f"> 用户昵称:{self.username}", 65 | f"> 上级用户:{self.upuid}", 66 | f"> 当前用户:{self.uid}", 67 | f"> 注册日期:{self.regtime}", 68 | f"> 当前币数:{int(self.score * 100)}" 69 | ] 70 | return "\n".join(msg_list) 71 | 72 | def __repr__(self): 73 | self.__str__() 74 | 75 | 76 | class RspRecommendReadCfg(BaseModel): 77 | """推荐 响应数据 ReadCfg""" 78 | check_score: float = Field(..., description="检查积分") 79 | user_score: float = Field(..., description="用户积分") 80 | 81 | 82 | class RspRecommendInfoView(BaseModel): 83 | """推荐 响应数据 InfoView""" 84 | num: float 85 | score: float 86 | rest: float 87 | status: int = Field(..., description=''' 88 | 当前阅读状态,目前已知: 89 | 当rest = 0 且 status = 1 时:等待下一批、任务上限 90 | 当rest = 4 且 status = 1 时: 等待开始阅读 91 | 当rest = 1 且 status = 3 时:不确定此状态 92 | 93 | rest暂时不知道是什么,但就目前已知情况,可简略推测 94 | 当 status = 1 时,表示此用户处于可正常阅读状态 95 | ''') 96 | msg: str = Field(None, description="当前阅读状态") 97 | 98 | def __str__(self): 99 | msg_list = [ 100 | f"{NestedLogColors.black('【阅读信息】')}", 101 | f"> 今日阅读篇数: {self.num}", 102 | f"> 今日阅读奖励: {self.score * 100}", 103 | f"> 转换成现金为: {self.score / 100}", 104 | f"{NestedLogColors.black('【阅读状态】')}", 105 | ] 106 | if self.status == 1: 107 | msg_list.append(f"> 🟢 正常阅读") 108 | elif self.status == 4: 109 | msg_list.append(f"> 🔴 {NestedLogColors.red('不可阅读')}") 110 | elif self.status == 3 and self.rest == 1 and self.num == 0 and self.score == 0: 111 | msg_list.append(f"> 🟡 {NestedLogColors.yellow('等待阅读')}") 112 | else: 113 | msg_list.append(f"> ⚪️ {NestedLogColors.white(f'未记录此状态码{self.status}(可通知作者添加)')}") 114 | 115 | if self.msg: 116 | msg_list.append(f"> {self.msg}") 117 | 118 | return "\n".join(msg_list) 119 | 120 | 121 | class RspRecommendData(BaseModel): 122 | """推荐 响应数据""" 123 | user: RspRecommendUser 124 | readCfg: RspRecommendReadCfg 125 | infoView: RspRecommendInfoView 126 | tips: str 127 | 128 | 129 | class RspRecommend(BaseModel): 130 | """推荐 响应""" 131 | code: int 132 | data: RspRecommendData 133 | 134 | 135 | class RspReadUrl(BaseModel): 136 | """获取阅读链接 响应""" 137 | link: str | HttpUrl = Field(..., alias="jump", description="阅读链接") 138 | 139 | @validator("link") 140 | def check_link(cls, v) -> URL: 141 | if not isinstance(v, URL): 142 | v = URL(v) 143 | return v 144 | 145 | 146 | # 检测有效阅读链接 147 | ARTICLE_LINK_VALID_COMPILE = re.compile( 148 | r"^https?://mp.weixin.qq.com/s\?__biz=[^&]*&mid=[^&]*&idx=\d*&(?!.*?chksm).*?&scene=\d*#wechat_redirect$") 149 | 150 | 151 | class RspDoRead(BaseModel): 152 | """完成阅读检查 响应""" 153 | check_finish: int | None = Field(None, 154 | description="是否完成阅读,目前了解到的是 1:阅读成功。此外如果这个字段不存在,则表示不处于检测阶段") 155 | success_msg: str | None = Field(None, description="阅读完成后的提示信息") 156 | jkey: str | None = Field(None, description="阅读链接的jkey,当检测未通过时,这个字段不会返回") 157 | url: str | None = Field(None, description="阅读链接,有一次返回的是None") 158 | msg: str | None = Field(None, description="这个字段目前已知当阅读达到上限时出现") 159 | 160 | @property 161 | def is_next_check(self) -> bool: 162 | """ 163 | 当前返回的链接是否是检测文章 164 | :return: True: 是检测文章 False: 不是检测文章 165 | """ 166 | if self.check_finish is None and self.success_msg is None: 167 | return True 168 | # 169 | return ARTICLE_LINK_VALID_COMPILE.match(self.url) is None 170 | 171 | @property 172 | def is_pass_failed(self) -> bool: 173 | """ 174 | 是否检测失败 175 | 176 | 例如: {'check_finish': 1, 'success_msg': '检测未通过', 'url': 'close'} 177 | """ 178 | if self.success_msg == "检测未通过" and self.url == "close": 179 | return True 180 | return False 181 | 182 | @property 183 | def is_check_success(self) -> bool: 184 | """上一次阅读检测文章链接是否检测成功""" 185 | if self.check_finish == 1 and self.success_msg == "阅读成功": 186 | return True 187 | return False 188 | 189 | @property 190 | def ret_count(self): 191 | """ 192 | 响应数据中的键值对个数 193 | 194 | 经过粗略的观察,一般来说: 195 | 返回2个,可能出现以下情况: 196 | - 此数据返回的结果检测状态未知,目前只能通过匹配特征biz的方式来筛选 197 | { 198 | "jkey": "MDAwMDAwMDAwM......", 199 | "url": "https://mp.weixin.qq.com/s?__biz=Mzg2OTYyNDY1OQ==&mid=2247649861&idx=1&sn=f0216ebeec1edb6c30ba1ab54a6fec7d&scene=0#wechat_redirect" 200 | } 201 | - 未知状态,现猜测是当前无文章可分配 202 | { 203 | "jkey": "MDAwMDAwMDAwMH6et2yHiryRsqu64L67gKOXfIvLlWra145qlc2LjI2BlKDGmIZmypO5qtTXu6iMaoh6nbSIfdLNmGvMmA", 204 | "url": null 205 | } 206 | { 207 | "msg": "异常访问,请重试", 208 | "url": "close" 209 | } 210 | - 检测失败 211 | { 212 | "success_msg": "检测未通过", 213 | "url": "close" 214 | } 215 | - 阅读完成 216 | { 217 | "success_msg": "本轮阅读已完成", 218 | "url": "close" 219 | } 220 | { 221 | "success_msg": "今天已达到阅读限制,请勿多个平台阅读", 222 | "url": "close" 223 | } 224 | 返回3个,分两种情况:检测失败、已通过检测并获得了奖励 225 | - 检测失败:{'check_finish': 1, 'success_msg': '检测未通过', 'url': 'close'} 226 | - 已通过检测并获得了奖励: 227 | { 228 | "success_msg": "阅读成功,获得110币", 229 | "jkey": "MDAwMDAw........", 230 | "url": "https://mp.weixin.qq.com/s?__biz=Mzg3Nzg4OTA4Ng==&mid=2247526971&idx=1&sn=2edf88cefcf3e30f988fab3f1f4f86de&scene=0#wechat_redirect" 231 | } 232 | 返回4个,应该表示正处于检测中,可以通过其中的 success_msg 获取阅读情况,目前已知的阅读情况有:阅读成功 233 | { 234 | "check_finish": 1, 235 | "success_msg": "阅读成功", 236 | "jkey": "MDAwMDAwMD.........", 237 | "url": "https://mp.weixin.qq.com/s?__biz=MjM5NDY4MTAwMA==&mid=2656010192&idx=1&sn=2f19bcd42822dff884fd1b3091732cbc&scene=0#wechat_redirect" 238 | } 239 | :return: 240 | """ 241 | count = 0 242 | if self.check_finish is not None: 243 | count += 1 244 | if self.success_msg is not None: 245 | count += 1 246 | if self.jkey is not None: 247 | count += 1 248 | if self.url is not None: 249 | count += 1 250 | if self.msg is not None: 251 | count += 1 252 | return count 253 | 254 | 255 | class RspWithdrawalUser(BaseModel): 256 | """提款用户信息""" 257 | uid: str 258 | username: str 259 | upuid: str | int | None = Field(None, description="上级ID") 260 | score: float 261 | weixinname: str 262 | u_ali_account: str | None = Field(None, description="支付宝账号") 263 | u_ali_real_name: str | None = Field(None, description="支付宝真实姓名") 264 | regtime: str 265 | super_user: float 266 | 267 | @property 268 | def amount(self): 269 | return int(self.score) 270 | 271 | def __str__(self): 272 | msg_list = [ 273 | f"{NestedLogColors.black('【提款用户信息】')}", 274 | f"> 用户ID: {self.uid}", 275 | f"> 用户名: {self.username}", 276 | f"> 上级ID: {self.upuid}", 277 | f"> 当前余额: {int(self.score) * 100}", 278 | f"> 微信昵称: {self.weixinname}", 279 | ] 280 | if self.u_ali_account: 281 | msg_list.append(f"> 支付宝账号: {self.u_ali_account}") 282 | if self.u_ali_real_name: 283 | msg_list.append(f"> 支付宝真实姓名: {self.u_ali_real_name}") 284 | if self.regtime: 285 | msg_list.append(f"> 注册时间: {self.regtime}") 286 | return "\n".join(msg_list) 287 | 288 | def __repr__(self): 289 | return self.__str__() 290 | 291 | 292 | class RspWithdrawalData(BaseModel): 293 | """提款信息""" 294 | user: RspWithdrawalUser 295 | kefuWx: str 296 | 297 | def __str__(self): 298 | return self.user.__str__() 299 | 300 | def __repr__(self): 301 | return self.__str__() 302 | 303 | 304 | class RspWithdrawal(BaseModel): 305 | code: int 306 | data: RspWithdrawalData 307 | 308 | def __str__(self): 309 | return self.data.__str__() 310 | 311 | def __repr__(self): 312 | return self.__str__() 313 | -------------------------------------------------------------------------------- /utils/push_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # push_utils.py created by MoMingLog on 29/3/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-03-29 6 | 【功能描述】 7 | """ 8 | import re 9 | import time 10 | 11 | import httpx 12 | 13 | from config import storage_cache_config, load_wx_business_access_token 14 | from utils import global_utils, md5 15 | 16 | 17 | class WxPusher: 18 | 19 | @classmethod 20 | def push_article(cls, appToken: str, title: str, link: str, uids: str | list = None, topicIds: str | list = None): 21 | content = f'''

{title}文章检测

''' 22 | print(f"🚛🚛 文章推送中 ->{link}") 23 | if WxPusher.push_core(appToken, title, content, url=link, uids=uids, topicIds=topicIds): 24 | print("> 🟢🟡 文章推送成功! 请尽快点击!") 25 | return True 26 | print("> 🔴❌️ 文章推送失败! ") 27 | return False 28 | 29 | @classmethod 30 | def push_msg(cls, appToken: str, title: str, content: str, uids: str | list = None, topicIds: str | list = None): 31 | if WxPusher.push_core(appToken, title, content, content_type=1, uids=uids, topicIds=topicIds): 32 | print("> 🟢🟡 消息推送成功!") 33 | return True 34 | print("> 🔴❌️ 消息推送失败! ") 35 | return False 36 | 37 | @classmethod 38 | def push_core(cls, appToken, title, content, url: str = None, content_type: int = 2, uids: str | list = None, 39 | topicIds: str | list = None): 40 | if isinstance(uids, str): 41 | uids = [uids] 42 | if isinstance(topicIds, str): 43 | topicIds = [topicIds] 44 | data = { 45 | "appToken": appToken, 46 | "content": content, 47 | "summary": title, 48 | "contentType": content_type, 49 | "uids": uids or [], 50 | "topicIds": topicIds or [], 51 | } 52 | if url: 53 | data["url"] = url 54 | 55 | url = "https://wxpusher.zjiecode.com/api/send/message" 56 | max_retry = 3 57 | while max_retry > 0: 58 | try: 59 | response = httpx.post(url, json=data) 60 | if response.json().get("code") == 1000: 61 | return True 62 | time.sleep(1) 63 | except Exception as e: 64 | print(f"Error occurred: {e}") 65 | time.sleep(1) 66 | max_retry -= 1 67 | return False 68 | 69 | 70 | class WxBusinessPusher: 71 | USER_NAME_COMPILE = re.compile(r"用户.*?(.*)") 72 | TURN_COUNT_COMPILE = re.compile(r"轮数.*?(\d+)") 73 | CHAPTER_COUNT_COMPILE = re.compile(r"篇数.*?(\d+)") 74 | READ_CHAPTER_COUNT_COMPILE = re.compile(r"已读.*?(\d+)") 75 | CURRENT_CHAPTER_COUNT_COMPILE = re.compile(r"当前.*?(\d+)") 76 | 77 | @staticmethod 78 | def handle_read_situation(situation: str | tuple, is_robot: bool = False): 79 | """ 80 | 处理阅读情况 81 | :return: 82 | """ 83 | user_info = None 84 | if is_robot: 85 | user_info = f"> 用户: {situation[0]}\n" 86 | else: 87 | if isinstance(situation, str): 88 | if r := WxBusinessPusher.USER_NAME_COMPILE.search(situation): 89 | user_info = f'
> 用户: {r.group(1)}
' 90 | else: 91 | user_info = f'
> 用户: {situation[0]}
' 92 | 93 | msg_list = [] 94 | if isinstance(situation, tuple): 95 | msg_list.extend([ 96 | f"> 轮数: {situation[1]}", 97 | f"> 篇数: {situation[2]}", 98 | f"> 已读: {situation[3]}", 99 | f"> 当前: {situation[4]}", 100 | ]) 101 | elif isinstance(situation, str): 102 | # 尝试按照固定格式提取 103 | if r := WxBusinessPusher.TURN_COUNT_COMPILE.search(situation): 104 | msg_list.append(f"> 轮数: {r.group(1)}") 105 | if r := WxBusinessPusher.CHAPTER_COUNT_COMPILE.search(situation): 106 | msg_list.append(f"> 篇数: {r.group(1)}") 107 | if r := WxBusinessPusher.READ_CHAPTER_COUNT_COMPILE.search(situation): 108 | msg_list.append(f"> 已读: {r.group(1)}") 109 | if r := WxBusinessPusher.CURRENT_CHAPTER_COUNT_COMPILE.search(situation): 110 | msg_list.append(f"> 当前: {r.group(1)}") 111 | 112 | return user_info + "\n".join(msg_list) 113 | 114 | @staticmethod 115 | def push_article_by_robot(webhook: str, title: str, link: str, is_markdown: bool = False, 116 | situation: str | tuple = None, tips: str = None, 117 | **kwargs): 118 | """ 119 | 通过企业微信机器人推送文章 120 | 121 | 参考文章:https://developer.work.weixin.qq.com/document/path/91770 122 | 123 | :param key: 124 | :return: 125 | """ 126 | 127 | if is_markdown: 128 | situation = WxBusinessPusher.handle_read_situation(situation, is_robot=True) 129 | msg_type = "markdown" 130 | s = f''' 131 | # {title} 132 | 133 | 【当前阅读情况】 134 | {situation} 135 | 136 | 【Tips】 137 | > {tips} 138 | 139 | ----> [前往阅读]({link}) 140 | 141 | ----> {global_utils.get_date()}''' 142 | else: 143 | s = link 144 | msg_type = "text" 145 | data = { 146 | "msgtype": msg_type, 147 | msg_type: { 148 | "content": s 149 | } 150 | } 151 | print(f"> 🚛🚛 文章推送中 ->{link}") 152 | max_retry = 3 153 | while max_retry > 0: 154 | try: 155 | response = httpx.post(webhook, json=data, verify=False) 156 | if response.json().get("errcode") == 0: 157 | print("> 🟢🟡 检测文章已推送! 请尽快点击!") 158 | return True 159 | finally: 160 | time.sleep(1) 161 | max_retry -= 1 162 | print("> 🔴❌️ 文章推送失败! ") 163 | return False 164 | 165 | @staticmethod 166 | def push_article_by_agent( 167 | corp_id: str, 168 | corp_secret: str, 169 | agent_id: int, 170 | title: str, 171 | link: str, 172 | situation: str | tuple, 173 | tips: str, 174 | token=None, 175 | recursion=0, 176 | **kwargs 177 | ): 178 | """ 179 | 通过企业微信中的应用来推送文章 180 | 181 | 创建应用教程参考:https://juejin.cn/post/7235078247238680637#heading-0 182 | 183 | 开发修改教程参考:https://developer.work.weixin.qq.com/document/path/90236#%E6%8E%A5%E5%8F%A3%E5%AE%9A%E4%B9%89 184 | 185 | :param corp_id: 公司ID 186 | :param corp_secret: 应用密钥(在创建的应用中,找到AgentId下方的Secret即可) 187 | :param agent_id: 应用ID 188 | :param title: 推送标题 189 | :param link: 详情链接 190 | :param situation: 阅读情况 191 | :param tips: 提示信息 192 | :param token: 缓存的accessToken 193 | :param recursion: 递归次数 194 | :return: 195 | """ 196 | situation = WxBusinessPusher.handle_read_situation(situation) 197 | data = { 198 | "touser": '@all', 199 | "agentid": agent_id, 200 | "msgtype": "textcard", 201 | "textcard": { 202 | "title": f"{title}", 203 | "description": '
{}
\ 204 |
【当前阅读情况】
{}
{}
'.format( 205 | global_utils.get_date(is_fill_chinese=True), situation, tips 206 | ), 207 | "url": link, 208 | "btntxt": "阅读文章" 209 | }, 210 | } 211 | if token is None: 212 | # 首先尝试从缓存中读取accessToken 213 | try: 214 | token = load_wx_business_access_token(corp_id, agent_id) 215 | except KeyError: 216 | # 如果报错,则进行请求获取 217 | token = WxBusinessPusher._get_accessToken(corp_id, corp_secret, agent_id) 218 | 219 | url = f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}" 220 | 221 | def re_get(): 222 | if recursion >= 3: 223 | raise Exception("> 🔴❌️ 递归次数达到上限,停止重新获取accessToken") 224 | else: 225 | print("> 🔴🟡 accessToken不合法/已过期!正在尝试重新获取...") 226 | token = WxBusinessPusher._get_accessToken(corp_id, corp_secret, agent_id) 227 | return WxBusinessPusher.push_article_by_agent(corp_id, corp_secret, agent_id, title, link, 228 | situation, 229 | tips, token, recursion + 1) 230 | 231 | retry = 3 232 | while retry > 0: 233 | try: 234 | response = httpx.post(url=url, json=data, verify=False) 235 | res_json = response.json() 236 | errcode = res_json.get("errcode") 237 | if errcode == 0: 238 | print("> 🟢🟡 检测文章已推送! 请尽快点击!") 239 | return True 240 | elif errcode == 40014: 241 | # 重新获取accessToken并推送 242 | return re_get() 243 | elif errcode == 42001: 244 | err_msg = res_json.get('errmsg') 245 | if "expired" in err_msg: 246 | # 重新获取accessToken并推送 247 | return re_get() 248 | raise Exception( 249 | f"> 🔴🟡 请求被拒绝,请确认您的IP被放入了白名单(企业可信IP), 具体响应如下:\n {err_msg}") 250 | else: 251 | print(f"出现其他推送失败情况,原数据:{res_json}") 252 | finally: 253 | time.sleep(1) 254 | retry -= 1 255 | print("> 🔴❌️ 文章推送失败! ") 256 | return False 257 | 258 | @staticmethod 259 | def _get_accessToken(corp_id, corp_secret, agent_id): 260 | """ 261 | 获取AccessToken 262 | :param corp_id: 企业ID 263 | :param corp_secret: 应用密钥(在创建的应用中,找到AgentId下方的Secret即可) 264 | :param agent_id: 应用ID 265 | :return: 266 | """ 267 | url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + corp_id + "&corpsecret=" + corp_secret 268 | p = httpx.get(url=url, verify=False) 269 | access_token = p.json()["access_token"] 270 | # expires_in = p.json()["expires_in"] # 没有必要,反正有几率提前失效,直接做好失效后的处理即可 271 | key = md5(f"{corp_id}_{agent_id}") 272 | # 缓存token 273 | storage_cache_config({ 274 | "wxBusiness": { 275 | key: { 276 | "accessToken": access_token, 277 | # "expiresIn": expires_in 278 | } 279 | } 280 | }) 281 | return access_token 282 | -------------------------------------------------------------------------------- /utils/logger_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # logger_utils.py created by MoMingLog on 29/3/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-03-29 6 | 【功能描述】 7 | """ 8 | import logging 9 | import os 10 | import threading 11 | 12 | from colorama import Fore, Style, init 13 | from httpx import Response, URL 14 | from pydantic import BaseModel 15 | 16 | from utils import get_date 17 | 18 | try: 19 | import ujson as json 20 | except ModuleNotFoundError: 21 | import json 22 | 23 | logs_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logs") 24 | 25 | lock = threading.Lock() 26 | 27 | 28 | class LogColors: 29 | DEBUG = Fore.WHITE 30 | INFO = Fore.GREEN 31 | WARNING = Fore.YELLOW 32 | ERROR = Fore.RED 33 | CRITICAL = Fore.RED 34 | 35 | 36 | class NestedLogColors: 37 | 38 | @staticmethod 39 | def colorize(content: str, net_color, log_color: str = None): 40 | color = "{{log-color}}" if log_color is None else getattr(Fore, log_color.upper()) 41 | return net_color + content + Style.RESET_ALL + color 42 | 43 | @staticmethod 44 | def white(content: str, color: str = None): 45 | return NestedLogColors.colorize(content, Fore.WHITE, color) 46 | 47 | @staticmethod 48 | def blue(content: str, color: str = None): 49 | return NestedLogColors.colorize(content, Fore.BLUE, color) 50 | 51 | @staticmethod 52 | def red(content: str, color: str = None): 53 | return NestedLogColors.colorize(content, Fore.RED, color) 54 | 55 | @staticmethod 56 | def green(content: str, color: str = None): 57 | return NestedLogColors.colorize(content, Fore.GREEN, color) 58 | 59 | @staticmethod 60 | def yellow(content: str, color: str = None): 61 | return NestedLogColors.colorize(content, Fore.YELLOW, color) 62 | 63 | @staticmethod 64 | def black(content: str, color: str = None): 65 | return NestedLogColors.colorize(content, Fore.BLACK, color) 66 | 67 | @staticmethod 68 | def cyan(content: str, color: str = None): 69 | return NestedLogColors.colorize(content, Fore.CYAN, color) 70 | 71 | @staticmethod 72 | def magenta(content: str, color: str = None): 73 | return NestedLogColors.colorize(content, Fore.MAGENTA, color) 74 | 75 | 76 | class Logger: 77 | def __init__(self, name: str, fh_level=logging.DEBUG, ch_level=logging.INFO, is_init_colorama=True): 78 | if is_init_colorama: 79 | init() 80 | self.logger = logging.getLogger(name) 81 | self.logger.setLevel(logging.DEBUG) 82 | path_dir = os.path.join(logs_dir, name) 83 | # 建立一个FileHandler,用于写入日志文件 84 | try: 85 | lock.acquire() 86 | if not os.path.exists(path_dir): 87 | os.makedirs(path_dir) 88 | except Exception as e: 89 | print(e) 90 | finally: 91 | lock.release() 92 | # 建立一个FileHandler,用于写入日志文件 93 | fh = logging.FileHandler(f"{path_dir}/{get_date()}.log", encoding="utf-8") 94 | fh.setLevel(fh_level) 95 | # 建立一个StreamHandler,用于输出到控制台 96 | ch = logging.StreamHandler() 97 | ch.setLevel(ch_level) 98 | # 设置文件日志格式 99 | fh_formatter = logging.Formatter('%(asctime)s - %(name)s \n=> %(levelname)s \n=> %(message)s \n') 100 | fh.setFormatter(fh_formatter) 101 | # 设置控制台日志格式 102 | ch_formatter = logging.Formatter(f'%(message)s') 103 | ch.setFormatter(ch_formatter) 104 | # 给logger添加handler 105 | self.logger.addHandler(fh) 106 | self.logger.addHandler(ch) 107 | self.is_set_prefix = False 108 | self.current_prefix_tag = "" 109 | 110 | def set_tag(self, tag): 111 | self.logger.handlers[0].setFormatter( 112 | logging.Formatter(f'%(asctime)s - %(name)s \n=> %(levelname)s \n=> {tag} \n=> %(message)s \n')) 113 | self.logger.handlers[1].setFormatter(logging.Formatter(f'{tag}[%(levelname)s] -> %(message)s')) 114 | 115 | def set_console_level(self, level): 116 | self.logger.handlers[1].setLevel(level) 117 | 118 | def set_file_level(self, level): 119 | self.logger.handlers[0].setLevel(level) 120 | 121 | def debug(self, msg, *args, prefix="", **kwargs): 122 | self.log(logging.DEBUG, msg, *args, prefix_tag=prefix, **kwargs) 123 | 124 | def info(self, msg, *args, prefix="", **kwargs): 125 | self.log(logging.INFO, msg, *args, prefix_tag=prefix, **kwargs) 126 | 127 | def war(self, msg, *args, prefix="", **kwargs): 128 | self.log(logging.WARNING, msg, *args, prefix_tag=prefix, **kwargs) 129 | 130 | def error(self, msg, *args, prefix="", **kwargs): 131 | self.log(logging.ERROR, msg, *args, prefix_tag=prefix, **kwargs) 132 | 133 | def cri(self, msg, *args, prefix="", **kwargs): 134 | self.log(logging.CRITICAL, msg, *args, prefix_tag=prefix, **kwargs) 135 | 136 | def exception(self, msg, *args, exc_info=True, **kwargs): 137 | self.log(logging.ERROR, msg, *args, exc_info=exc_info, **kwargs) 138 | 139 | def log(self, level, msg, *args, prefix_tag="", **kwargs): 140 | lock.acquire() 141 | if isinstance(msg, BaseModel): 142 | msg = msg.__str__() 143 | elif isinstance(msg, Exception): 144 | msg = str(msg) 145 | elif isinstance(msg, URL): 146 | msg = str(msg) 147 | elif isinstance(msg, object): 148 | msg = msg.__str__() 149 | 150 | level_name = logging.getLevelName(level) 151 | level_color = getattr(LogColors, level_name) 152 | msg = msg.replace("{{log-color}}", level_color) 153 | msg = f"{level_color}{msg}{Style.RESET_ALL}" 154 | # if prefix and not self.is_set_prefix: 155 | if prefix_tag and prefix_tag != self.current_prefix_tag: 156 | # print(f"开始重置前缀 {prefix_tag}: {self.current_prefix_tag}") 157 | self.current_prefix_tag = prefix_tag 158 | color_prefix_tag = f"{Fore.BLACK}{prefix_tag}{Style.RESET_ALL}" 159 | self.logger.handlers[0].setFormatter( 160 | logging.Formatter(f'%(asctime)s - %(name)s \n=> %(levelname)s \n=> {prefix_tag} \n=> %(message)s \n')) 161 | self.logger.handlers[1].setFormatter(logging.Formatter(f'{color_prefix_tag}[%(levelname)s] -> %(message)s')) 162 | # else: 163 | # print(f"前缀未变化:{prefix_tag}: {self.current_prefix_tag}") 164 | self.logger.log(level, msg, *args, **kwargs) 165 | lock.release() 166 | 167 | def response( 168 | self, 169 | prefix: str, 170 | response: Response, 171 | *args, 172 | prefix_tag="", 173 | print_request_url=True, 174 | print_request_method=True, 175 | print_request_headers=True, 176 | print_request_cookies=True, 177 | print_request_body=True, 178 | print_response_headers=True, 179 | print_response_body=True, 180 | print_text_all=False, 181 | print_text_limit=1000, 182 | **kwargs 183 | ): 184 | prefix = prefix.center(30, "*") 185 | 186 | msg_list = [prefix, f"==> 【响应状态码】{response.status_code}"] 187 | 188 | if print_request_url: 189 | msg_list.append(f"==> 【请求地址】{response.url}") 190 | if print_request_method: 191 | msg_list.append(f"==> 【请求方法】{response.request.method}") 192 | if print_request_headers: 193 | msg_list.append(f"==> 【请求头】") 194 | for key, value in response.request.headers.items(): 195 | msg_list.append(f"{key}: {value}") 196 | if print_request_body: 197 | body = response.request.content.decode(encoding='utf-8') 198 | if body: 199 | msg_list.append(f"==> 【请求数据】") 200 | msg_list.append(body) 201 | if print_response_headers: 202 | msg_list.append(f"==> 【响应头】") 203 | for key, value in response.headers.items(): 204 | msg_list.append(f"{key}: {value}") 205 | if print_response_body: 206 | content = response.text 207 | if content: 208 | msg_list.append(f"==> 【响应数据】") 209 | # 判断是否为json 210 | try: 211 | content = json.loads(content) 212 | content = json.dumps(content, ensure_ascii=False, indent=2) 213 | except json.JSONDecodeError: 214 | if not print_text_all: 215 | # 判断长度是否超过1000 216 | if len(content) > print_text_limit: 217 | content = content[:print_text_limit] + f"...(超过{print_text_limit},已省略)" 218 | 219 | msg_list.append(content) 220 | 221 | suffix = prefix.center(30, "*") 222 | msg_list.append(suffix) 223 | 224 | self.logger.debug(Fore.WHITE + "\n".join(msg_list) + Style.RESET_ALL) 225 | 226 | 227 | class ThreadLogger(Logger): 228 | def __init__(self, name: str, thread2name: dict = None, is_init_colorama: bool = True): 229 | super().__init__(name, is_init_colorama=is_init_colorama) 230 | self.thread2name = thread2name 231 | 232 | @property 233 | def name(self): 234 | try: 235 | lock.acquire() 236 | thread_name = threading.current_thread().name 237 | if thread_name == "MainThread": 238 | return "" 239 | else: 240 | thread_name = self.thread2name[threading.current_thread().ident] 241 | return thread_name 242 | finally: 243 | lock.release() 244 | 245 | 246 | 247 | @property 248 | def is_log_response(self): 249 | return self.thread2name.get('is_log_response', False) 250 | 251 | def info(self, msg, *args, **kwargs): 252 | ident = kwargs.pop("ident", None) 253 | if ident is not None: 254 | name = self.thread2name.get(ident, "") 255 | else: 256 | name = self.name 257 | super().info(msg, *args, prefix=name, **kwargs) 258 | 259 | def debug(self, msg, *args, **kwargs): 260 | ident = kwargs.pop("ident", None) 261 | if ident is not None: 262 | name = self.thread2name.get(ident, "") 263 | else: 264 | name = self.name 265 | super().debug(msg, *args, prefix=name, **kwargs) 266 | 267 | def war(self, msg, *args, **kwargs): 268 | ident = kwargs.pop("ident", None) 269 | if ident is not None: 270 | name = self.thread2name.get(ident, "") 271 | else: 272 | name = self.name 273 | super().war(msg, *args, prefix=name, **kwargs) 274 | 275 | def error(self, msg, *args, **kwargs): 276 | ident = kwargs.pop("ident", None) 277 | if ident is not None: 278 | name = self.thread2name.get(ident, "") 279 | else: 280 | name = self.name 281 | super().error(msg, *args, prefix=name, **kwargs) 282 | 283 | def cri(self, msg, *args, **kwargs): 284 | ident = kwargs.pop("ident", None) 285 | if ident is not None: 286 | name = self.thread2name.get(ident, "") 287 | else: 288 | name = self.name 289 | super().cri(msg, *args, prefix=name, **kwargs) 290 | 291 | def response(self, prefix: str, response: Response, *args, **kwargs): 292 | if not self.is_log_response: 293 | return 294 | ident = kwargs.pop("ident", None) 295 | if ident is not None: 296 | name = self.thread2name.get(ident, "") 297 | else: 298 | name = self.name 299 | super().response( 300 | prefix, 301 | response, 302 | *args, 303 | prefix_tag=name, 304 | **kwargs 305 | ) 306 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /script/v2/ltwm_v2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ltwm_v2.py created by MoMingLog on 4/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-04 6 | 【功能描述】 7 | """ 8 | import re 9 | import time 10 | 11 | from httpx import URL 12 | 13 | from config import load_ltwm_config 14 | from exception.common import StopReadingNotExit, PauseReadingTurnNext, CookieExpired 15 | from schema.ltwm import LTWMConfig, UserPointInfo, LTWMAccount, TaskList, ReaderDomain, GetTokenByWxKey, ArticleUrl, \ 16 | CompleteRead, Sign, BalanceWithdraw 17 | from script.common.base import WxReadTaskBase 18 | 19 | 20 | class APIS: 21 | # 通用API前缀 22 | COMMON = "/api/mobile" 23 | 24 | # API: 获取用户积分信息 25 | USER_ACCOUNT = f"{COMMON}/userCenter/v1/userAccount" 26 | # API: 获取当前任务列表信息 27 | TASK_LIST = f"{COMMON}/task/v1/taskList" 28 | 29 | # 通用阅读任务前缀 30 | COMMON_READ = f"{COMMON}/act/officialArticle/v1" 31 | 32 | # 阅读任务API: 获取阅读链接 33 | GET_READ_DOMAIN = f"{COMMON_READ}/getReaderDomain" 34 | # 阅读任务API: 可能是重置或者获取新的Authorization值,也有可能是将Auth值与domain的key值绑定起来 35 | GET_TOKEN_BY_WX_KEY = f"{COMMON_READ}/getTokenByWxKey" # 这个路径并不完整,后面还需要拼接上key值路径 36 | # 阅读任务API: 获取文章阅读地址 37 | GET_ARTICLE_URL = f"{COMMON_READ}/getArticle" 38 | # 阅读任务API: 阅读完成上报地址 39 | COMPLETE_READ = f"{COMMON_READ}/completeRead" 40 | 41 | # 签到任务API 42 | SIGN = f"{COMMON}/act/sign/v1/sign" 43 | # API: 申请提现 44 | WITHDRAW = f"{COMMON}/withdraw/v1/requestBalanceWithdraw" 45 | # API: 提现记录 46 | WITHDRAW_DETAIL = f"{COMMON}/detail/v1/pageAccountWithdraw" 47 | 48 | 49 | class LTWMV2(WxReadTaskBase): 50 | # 当前脚本作者 51 | CURRENT_SCRIPT_AUTHOR = "MoMingLog" 52 | # 当前脚本版本 53 | CURRENT_SCRIPT_VERSION = "2.0.0" 54 | # 当前脚本创建时间 55 | CURRENT_SCRIPT_CREATED = "2024-04-04" 56 | # 当前脚本更新时间 57 | CURRENT_SCRIPT_UPDATED = "2024-04-04" 58 | # 当前任务名称 59 | CURRENT_TASK_NAME = "力天微盟" 60 | 61 | # 当前使用的API域名(这里选择包含protocol) 62 | CURRENT_API_DOMAIN = "https://api.mb.s8xnldd7kpd.litianwm.cn" 63 | 64 | # 提取“获取域名”操作返回的key值 65 | FETCH_KEY_COMPILE = re.compile(r"key=(.*)") 66 | 67 | def __init__(self, config_data: LTWMConfig = load_ltwm_config(), run_read_task: bool = True): 68 | self.run_read_task = run_read_task 69 | self.keep_alive_set = set() 70 | super().__init__(config_data, logger_name="力天微盟") 71 | 72 | def init_fields(self, retry_count=3): 73 | pass 74 | 75 | def run(self, name, *args, **kwargs): 76 | # 配置基本URL 77 | self.base_client = self._get_client("base", headers=self.build_base_headers(account_config=self.accounts), 78 | base_url=self.CURRENT_API_DOMAIN, verify=False) 79 | self.base_client.headers.update({ 80 | "Authorization": self.account_config.authorization 81 | }) 82 | # 获取用户积分信息,并输出 83 | user_account = self.__request_user_account() 84 | if user_account.data is None: 85 | if "重新登录" in user_account.message: 86 | raise CookieExpired() 87 | else: 88 | raise StopReadingNotExit(user_account.message) 89 | 90 | if user_account.code == 500: 91 | raise StopReadingNotExit(user_account.message) 92 | else: 93 | self.logger.info(user_account) 94 | 95 | self.current_balance = user_account.data.balance 96 | 97 | if not self.run_read_task: 98 | self.__request_withdraw() 99 | return 100 | 101 | # 判断积分是否可以提现 102 | if self.current_balance >= 1000: 103 | self.logger.war(f"🟡💰 当前积分满足提现要求,开始提现...") 104 | self.__request_withdraw() 105 | 106 | # 获取用户任务列表 107 | task_list = self.__request_taskList() 108 | is_wait = False 109 | 110 | article_reward = 0 111 | # 检查当前任务还有哪些未完成 112 | for data in task_list.data: 113 | if "文章阅读" in data.name: 114 | 115 | if data.taskRemainTime != 0 and data.status == 2: 116 | self.logger.info(f"🟢 当前阅读任务已完成,{data.taskRemainTime}分钟后可继续阅读") 117 | # self.wait_queue.put(data.taskRemainTime) 118 | # self.wait_queue.put(name) 119 | # self.keep_alive_set.add(name) 120 | 121 | elif data.taskRemainTime == 0 and data.status == 4: 122 | self.logger.info(f"🟢 今日阅读任务已完成!") 123 | else: 124 | self.logger.war(f"🟡 检测到阅读任务待完成,3秒后开始执行...") 125 | # 提取数字 126 | article_reward = int(re.findall(r"\d+", data.remark)[0]) 127 | self.logger.info(f"阅读任务完成后积分将达到:{self.current_balance + article_reward}") 128 | time.sleep(3) 129 | try: 130 | self.__do_read_task() 131 | except Exception as e: 132 | if "本轮阅读成功完成,奖励发放中" in str(e) or "今天任务已完成" in str(e): 133 | is_wait = True 134 | self.current_balance += article_reward 135 | continue 136 | self.logger.exception(f"🔴 阅读任务异常:{e}") 137 | if "每日签到" in data.name: 138 | if self.current_balance >= 1000: 139 | self.logger.war(f"🟡💰 当前积分 [{self.current_balance}] 满足提现要求,开始提现...") 140 | self.__request_withdraw(is_wait) 141 | if data.status == 4: 142 | self.logger.info(f"🟢 今日签到任务已完成! 如有问题请反馈!") 143 | else: 144 | self.__do_sign_task() 145 | 146 | if self.current_balance >= 1000: 147 | self.logger.war(f"🟡💰 当前积分 [{self.current_balance}] 满足提现要求,开始提现...") 148 | self.__request_withdraw(is_wait) 149 | else: 150 | self.logger.war(f"🟡💰 当前积分 [{self.current_balance}] 不满足提现要求,停止提现") 151 | 152 | # 创建线程保活cookie 153 | # self.keep_alive_thread = threading.Thread(target=self.__do_keep_alive) 154 | # self.keep_alive_thread.start() 155 | 156 | # def __do_keep_alive(self): 157 | # """ 158 | # 尝试保活cookie 159 | # :return: 160 | # """ 161 | # try: 162 | # 163 | # self.logger.info("🟢 cookie保活成功") 164 | # except Exception as e: 165 | # self.logger.warning(f"🔴 cookie保活失败:{e}") 166 | 167 | def __do_sign_task(self): 168 | sign_model = self.__request_sign() 169 | if sign_model.data: 170 | self.logger.info(sign_model.data) 171 | self.current_balance += sign_model.data.currentIntegral 172 | else: 173 | self.logger.war(f"{sign_model.message}") 174 | 175 | def __do_read_task(self): 176 | # self.logger.info(task_list) 177 | # 获取用户阅读链接 178 | self.logger.war("🟡 正在获取阅读链接...") 179 | time.sleep(1) 180 | reader_domain = self.__request_reader_domain() 181 | if url := reader_domain.data: 182 | self.logger.info(f"🟢 阅读链接获取成功: {url}") 183 | # 判断url中是否有换行符,如存在,则置空 184 | url = url.replace("\n", "") 185 | url = URL(url) 186 | self.base_client.headers.update({ 187 | "Origin": f"{url.scheme}://{url.host}", 188 | "Referer": f"{url.scheme}://{url.host}" 189 | }) 190 | # self.parse_base_url(read_domain.data, self.read_client) 191 | self.docking_key = url.params.get("key") 192 | else: 193 | raise StopReadingNotExit(f"阅读链接获取失败!") 194 | 195 | # 开始对接阅读池 196 | self.logger.war("🟡 正在对接阅读池...") 197 | time.sleep(1.5) 198 | docking_model = self.__request_docking() 199 | if "操作成功" in docking_model.message: 200 | self.logger.info("🟢 阅读池对接成功!") 201 | if docking_model.data is not None: 202 | # 无论是否是原来的 auth,这里都进行更新一下,以防万一 203 | self.base_client.headers.update({ 204 | "Authorization": docking_model.data 205 | }) 206 | else: 207 | self.logger.error("🔴 阅读池对接失败,请联系作者更新!") 208 | # 开始提取阅读文章地址 209 | self.logger.war("🟡 正在抽取阅读文章...") 210 | time.sleep(1.5) 211 | article_model = self.__request_article_url() 212 | if "文章地址获取成功" in article_model.message: 213 | if article_url := article_model.data.articleUrl: 214 | self.logger.info(f"🟢 文章抽取成功! ") 215 | self.logger.info(article_model) 216 | # 打印文章信息 217 | # self.logger.info(self.parse_wx_article(article_url)) 218 | else: 219 | self.logger.war(f"🟠 文章地址为空,请检查!") 220 | else: 221 | self.logger.error("🔴 阅读文章抽取失败,请联系作者更新!") 222 | 223 | data = { 224 | "readKey": article_model.data.readKey, 225 | "taskKey": article_model.data.taskKey 226 | } 227 | 228 | while True: 229 | self.sleep_fun(False) 230 | # 上报阅读结果 231 | complete_model = self.__request_complete_read(data) 232 | 233 | if complete_model.code == 200: 234 | if "阅读任务上报成功" in complete_model.message: 235 | self.logger.info(f"🟢 阅读任务上报成功") 236 | self.logger.info(complete_model) 237 | data = { 238 | "readKey": complete_model.data.readKey, 239 | "taskKey": complete_model.data.taskKey 240 | } 241 | elif "本轮阅读成功" in complete_model.message: 242 | raise PauseReadingTurnNext(complete_model.message) 243 | else: 244 | raise StopReadingNotExit(f"阅读任务上报失败, {complete_model.message}") 245 | 246 | def __request_withdraw(self, is_wait: bool = False): 247 | # 判断是否要进行提现操作 248 | if not self.is_withdraw: 249 | self.logger.war(f"🟡 提现开关已关闭,已停止提现任务") 250 | return 251 | 252 | if is_wait: 253 | self.logger.info("5秒后开始提现, 请稍后") 254 | time.sleep(5) 255 | # 发起查询请求,查看当前用户积分 256 | user_model = self.__request_user_account() 257 | self.current_balance = user_model.data.balance 258 | if self.current_balance > 1000: 259 | withdraw_model = self.__request_do_withdraw() 260 | if "成功" in withdraw_model.message: 261 | self.logger.info(f"🟢💰 提现成功! \n {withdraw_model}") 262 | self.current_balance -= 1000 263 | # 顺便请求下提现详情 264 | self.__request_withdraw_detail() 265 | else: 266 | self.logger.error(f"🔴💰 提现失败, {withdraw_model.message}") 267 | else: 268 | self.logger.war(f"🟡 当前积分{user_model.data.balance}不满足最低提现要求, 脚本结束!") 269 | 270 | def __request_withdraw_detail(self): 271 | return self.request_for_json( 272 | "POST", 273 | APIS.WITHDRAW_DETAIL, 274 | "提现详情数据", 275 | client=self.base_client, 276 | data={} 277 | ) 278 | 279 | def __request_do_withdraw(self) -> BalanceWithdraw | dict: 280 | 281 | return self.request_for_json( 282 | "POST", 283 | APIS.WITHDRAW, 284 | "提现", 285 | client=self.base_client, 286 | model=BalanceWithdraw, 287 | json={} 288 | ) 289 | 290 | def __request_sign(self) -> Sign | dict: 291 | """发起签到请求""" 292 | return self.request_for_json( 293 | "GET", 294 | APIS.SIGN, 295 | "签到请求 base_client", 296 | client=self.base_client, 297 | model=Sign 298 | ) 299 | 300 | def __request_complete_read(self, data: dict) -> CompleteRead | dict: 301 | """ 302 | 阅读上报 303 | :return: 304 | """ 305 | return self.request_for_json( 306 | "POST", 307 | APIS.COMPLETE_READ, 308 | "阅读任务上报 base_client", 309 | client=self.base_client, 310 | model=CompleteRead, 311 | json=data 312 | ) 313 | 314 | def __request_article_url(self) -> ArticleUrl | dict: 315 | """获取文章阅读地址""" 316 | return self.request_for_json( 317 | "GET", 318 | APIS.GET_ARTICLE_URL, 319 | "获取文章阅读地址 base_client", 320 | client=self.base_client, 321 | model=ArticleUrl 322 | ) 323 | 324 | def __request_docking(self) -> GetTokenByWxKey | dict: 325 | """请求阅读对接,对接成功会返回用户的auth,或许也会返回新的,目前未知""" 326 | return self.request_for_json( 327 | "GET", 328 | f"{APIS.GET_TOKEN_BY_WX_KEY}/{self.docking_key}", 329 | "请求对接阅读池 base_client", 330 | client=self.base_client, 331 | model=GetTokenByWxKey 332 | ) 333 | 334 | def __request_reader_domain(self) -> ReaderDomain | dict: 335 | """获取正在进行阅读操作的用户对应的domain""" 336 | return self.request_for_json( 337 | "GET", 338 | APIS.GET_READ_DOMAIN, 339 | "获取专属阅读链接 base_client", 340 | client=self.base_client, 341 | model=ReaderDomain 342 | ) 343 | 344 | def __request_taskList(self) -> TaskList | dict: 345 | """获取任务列表信息""" 346 | return self.request_for_json( 347 | "GET", 348 | APIS.TASK_LIST, 349 | "获取任务列表信息 base_client", 350 | client=self.base_client, 351 | model=TaskList 352 | ) 353 | 354 | def __request_user_account(self) -> UserPointInfo | dict: 355 | """获取用户积分信息""" 356 | return self.request_for_json( 357 | "GET", 358 | APIS.USER_ACCOUNT, 359 | "获取用户积分信息 base_client", 360 | client=self.base_client, 361 | model=UserPointInfo, 362 | ) 363 | 364 | def build_base_headers(self, account_config: LTWMConfig = None): 365 | entry_url = self.get_entry_url() 366 | header = super().build_base_headers() 367 | header.update({ 368 | "Origin": entry_url, 369 | "Referer": entry_url, 370 | }) 371 | return header 372 | 373 | def get_entry_url(self) -> str: 374 | return "http://e9adf325c38844188a2f0aefaabb5e0d.op20skd.toptomo.cn/?fid=12286" 375 | 376 | @property 377 | def current_balance(self): 378 | return self._cache.get(f"current_balance_{self.ident}") 379 | 380 | @current_balance.setter 381 | def current_balance(self, value): 382 | self._cache[f"current_balance_{self.ident}"] = value 383 | 384 | @property 385 | def docking_key(self): 386 | return self._cache.get(f"docking_key_{self.ident}") 387 | 388 | @docking_key.setter 389 | def docking_key(self, value): 390 | if not value or value is None: 391 | raise StopReadingNotExit("key不能为空,已停止对接") 392 | 393 | self._cache[f"docking_key_{self.ident}"] = value 394 | 395 | @property 396 | def account_config(self) -> LTWMAccount: 397 | return super().account_config 398 | 399 | 400 | if __name__ == '__main__': 401 | LTWMV2() 402 | -------------------------------------------------------------------------------- /script/v2/ymz_v2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ymz_v2.py created by MoMingLog on 17/4/2024. 3 | """ 4 | 【作者】MoMingLog 5 | 【创建时间】2024-04-17 6 | 【功能描述】 7 | """ 8 | import base64 9 | import re 10 | import sys 11 | import time 12 | from io import BytesIO 13 | 14 | from PIL import Image 15 | from httpx import URL 16 | from pyzbar.pyzbar import decode 17 | 18 | from config import load_ymz_config 19 | from exception.common import RegExpError, PauseReadingAndCheckWait 20 | from schema.ymz import YMZConfig, RspTaskList, RspLogin, RspArticleUrl, RspSignIn, RspWithdrawOptions, RspUserInfo 21 | from script.common.base import WxReadTaskBase 22 | 23 | 24 | class APIS: 25 | COMMON = "/ttz/api" 26 | 27 | # API: 登录 28 | LOGIN = f"{COMMON}/login" 29 | # API: 获取用户信息 30 | GET_USER_INFO = f"{COMMON}/queryUserSumScoreById" 31 | # API: 获取任务列表信息 32 | GET_TASK_LIST = f"{COMMON}/queryActivityContentList" 33 | # API: 设置密码 34 | SET_WITHDRAW_PWD = f"{COMMON}/setUserCashPwd" 35 | # API: 阅读二维码 36 | READ_QR_CODE = f"{COMMON}/queryActivityContentx" 37 | 38 | # API: 获取文章链接(程序自动获取) 39 | GET_ARTICLE = "" 40 | 41 | # API: 签到 42 | SIGN_IN = f"{COMMON}//userSignin" 43 | # API: 提现选项 44 | WITHDRAW_OPTIONS = f"{COMMON}/queryMoneyInfo" 45 | # API: 提现 46 | WITHDRAW = "/ttz/pay/pocketMoney" 47 | 48 | 49 | class YMZV2(WxReadTaskBase): 50 | # 当前脚本作者 51 | CURRENT_SCRIPT_AUTHOR = "MoMingLog" 52 | # 当前脚本版本 53 | CURRENT_SCRIPT_VERSION = "2.0.0" 54 | # 当前脚本创建时间 55 | CURRENT_SCRIPT_CREATED = "2024-04-17" 56 | # 当前脚本更新时间 57 | CURRENT_SCRIPT_UPDATED = "2024-04-17" 58 | # 当前任务名称 59 | CURRENT_TASK_NAME = "有米赚" 60 | 61 | CURRENT_BASE_URL = "http://xingeds.3fexgd.zhijianzzmm.cn" 62 | CURRENT_ORIGIN_URL = "http://gew.gewxg.234tr.zhijianzzmm.cn" 63 | 64 | # 加载页正则,主要提取 originPath(包括链接) 65 | LOADING_PAGE_COMPILE = re.compile(r"var\soriginPath\s*=\s*['\"](.*?)['\"];?", re.S) 66 | # 提取获取文章链接的API 67 | GET_ARTICLE_API_COMPILE = re.compile(r"(?= money: 188 | result = self.__request_withdraw(money, id) 189 | self.logger.info(result) 190 | break 191 | 192 | def do_sign_in_task(self, task_name): 193 | self.logger.war(f"任务 [{task_name}] 未完成,正在尝试签到...") 194 | result = self.__request_sign_in() 195 | if result.get("code") == 200: 196 | self.logger.info(f"✅️ 任务 [{task_name}] {result}") 197 | else: 198 | self.logger.err(f"❌ 任务 [{task_name}] 签到失败, 原始响应数据为: {result}") 199 | 200 | def do_read_task(self, task_name): 201 | self.logger.war(f"任务 [{task_name}] 未完成,正在尝试获取二维码...") 202 | result = self.__request_qr_code_img_data() 203 | if result.get("code") == 200: 204 | self.logger.info(f"✅️ 任务 [{task_name}] 获取二维码成功") 205 | data = result.get("data", {}).get("twoMicrocodeUrl", "").replace("data:image/png;base64,", "") 206 | # 将Base64编码的字符串转换为字节 207 | image_data = base64.b64decode(data) 208 | 209 | # 使用BytesIO将字节转换为二进制流 210 | image = Image.open(BytesIO(image_data)) 211 | 212 | # 使用pyzbar的decode函数解析二维码 213 | decoded_objects = decode(image) 214 | # 打印解析结果 215 | for obj in decoded_objects: 216 | url = URL(obj.data.decode("utf-8")) 217 | self.origin_token = url.params.get("token") 218 | self.logger.info(f"✅️ 任务 [{task_name}] 二维码解析成功:{url}") 219 | 220 | loading_page = self.__request_read_loading_page(url) 221 | if loading_page: 222 | self.logger.info(f"✅️ 任务 [{task_name}] 二维码跳转成功") 223 | if r := self.LOADING_PAGE_COMPILE.search(loading_page): 224 | if len(r.groups()) != 1: 225 | raise RegExpError(self.LOADING_PAGE_COMPILE) 226 | # 提取originPath 227 | origin_path = r.group(1) 228 | self.base_read_url = URL(origin_path) 229 | # 更新read_client的base_url 230 | self.parse_base_url(origin_path, self.read_client) 231 | else: 232 | raise RegExpError(self.LOADING_PAGE_COMPILE) 233 | 234 | if r := self.GET_ARTICLE_API_COMPILE.search(loading_page): 235 | if len(r.groups()) != 1: 236 | raise RegExpError(self.GET_ARTICLE_API_COMPILE) 237 | start_num = 0 238 | while True: 239 | # 构建获取文章链接API 240 | APIS.GET_ARTICLE = f"{self.base_read_url.path}{r.group(1)}{self.modify_token(start_num)}" 241 | 242 | article_url_model = self.__request_get_article_url() 243 | print(article_url_model) 244 | if article_url_model: 245 | if article_url := article_url_model.data.url: 246 | self.logger.info(self.parse_wx_article(article_url)) 247 | start_num = article_url_model.data.startNum 248 | end_num = article_url_model.data.endNum 249 | self.logger.info(f"🟡 任务 [{task_name}] 当前进度:{start_num}/{end_num}") 250 | if start_num == end_num or start_num is None: 251 | self.logger.suc(f"✅️ 任务 [{task_name}] 完成") 252 | break 253 | self.sleep_fun() 254 | else: 255 | self.logger.war( 256 | f"🟡 任务 [{task_name}] 链接貌似获取失败了,原始响应为:{article_url_model}") 257 | return 258 | else: 259 | self.logger.err(f"任务 [{task_name}] 获取文章链接请求失败") 260 | return 261 | else: 262 | raise RegExpError(self.GET_ARTICLE_API_COMPILE) 263 | else: 264 | self.logger.err(f"任务 [{task_name}] 二维码跳转失败") 265 | return 266 | else: 267 | self.logger.err(f"任务 [{task_name}] 获取二维码失败") 268 | return 269 | 270 | def __request_withdraw(self, money, moneyId): 271 | return self.request_for_json( 272 | "GET", 273 | f"{APIS.WITHDRAW}?userShowId={self.user_id}&money={money}&wdPassword={self.pwd}&moneyId={moneyId}", 274 | "请求提现 base_client", 275 | client=self.base_client 276 | ) 277 | 278 | def __request_withdraw_options(self) -> RspWithdrawOptions | dict: 279 | return self.request_for_json( 280 | "GET", 281 | APIS.WITHDRAW_OPTIONS, 282 | "请求提现选项 base_client", 283 | client=self.base_client, 284 | model=RspWithdrawOptions 285 | ) 286 | 287 | def __request_sign_in(self) -> RspSignIn | dict: 288 | return self.request_for_json( 289 | "POST", 290 | f"{APIS.SIGN_IN}?userShowId={self.user_id}", 291 | "请求签到 base_client", 292 | client=self.base_client, 293 | model=RspSignIn 294 | ) 295 | 296 | def __request_get_article_url(self) -> RspArticleUrl | dict: 297 | return self.request_for_json( 298 | "GET", 299 | APIS.GET_ARTICLE, 300 | "请求文章链接 base_client", 301 | client=self.read_client, 302 | model=RspArticleUrl 303 | ) 304 | 305 | def __request_read_loading_page(self, read_url: str | URL): 306 | return self.request_for_page( 307 | read_url, 308 | "请求阅读加载页面 base_client", 309 | client=self.read_client 310 | ) 311 | 312 | def __request_qr_code_img_data(self): 313 | return self.request_for_json( 314 | "GET", 315 | f"{APIS.READ_QR_CODE}?userShowId={self.user_id}&type=1", 316 | "请求二维码 base_client", 317 | client=self.base_client 318 | ) 319 | 320 | def __request_task_list(self) -> RspTaskList | dict: 321 | return self.request_for_json( 322 | "GET", 323 | f"{APIS.GET_TASK_LIST}?userShowId={self.user_id}", 324 | "请求任务列表 base_client", 325 | client=self.base_client, 326 | 327 | model=RspTaskList 328 | ) 329 | 330 | def __request_userinfo(self) -> RspUserInfo | dict: 331 | return self.request_for_json( 332 | "GET", 333 | f"{APIS.GET_USER_INFO}?userShowId={self.user_id}", 334 | "请求用户信息 base_client", 335 | client=self.base_client, 336 | model=RspUserInfo 337 | ) 338 | 339 | def __request_set_pwd(self): 340 | return self.request_for_json( 341 | "GET", 342 | f"{APIS.SET_WITHDRAW_PWD}?userShowId={self.user_id}&wdPassword={self.pwd}&rewdPassword={self.pwd}", 343 | "设置提现密码 base_client", 344 | client=self.base_client 345 | ) 346 | 347 | def __request_login(self) -> RspLogin | dict: 348 | return self.request_for_json( 349 | "POST", 350 | APIS.LOGIN, 351 | "请求登录 base_client", 352 | client=self.base_client, 353 | update_headers={ 354 | "Content-Type": "application/x-www-form-urlencoded", 355 | "Origin": self.CURRENT_ORIGIN_URL, 356 | "Referer": f"{self.CURRENT_ORIGIN_URL}/" 357 | }, 358 | data={"userShowId": self.user_id}, 359 | model=RspLogin 360 | ) 361 | 362 | def modify_token(self, number: int = 0): 363 | """ 364 | 模拟JS中 365 | token = btoa(atob(token)+"&startNumber="+number) 366 | 的行为 367 | :param number: 固定从0开始,后面请求getArticleListxAuto接口后,才会更新number 368 | :return: 369 | """ 370 | # 解码 base64 字符串 371 | decoded_token = base64.b64decode(self.origin_token).decode('utf-8') 372 | 373 | # 添加新的参数 374 | modified_string = decoded_token + "&startNumber=" + str(number) 375 | 376 | # 重新编码为 base64 377 | encoded_token = base64.b64encode(modified_string.encode('utf-8')).decode('utf-8') 378 | 379 | return encoded_token 380 | 381 | def get_entry_url(self) -> str: 382 | return "http://i3n0nzg2wcvnhzu6opsu.xoa8m3pb4.zhijianzzmm.cn/ttz/wechat/ttzScanCode?userShowId=5332" 383 | 384 | @property 385 | def origin_token(self): 386 | return self._cache.get(f"origin_token_{self.user_id}_{self.ident}", ) 387 | 388 | @origin_token.setter 389 | def origin_token(self, value: str): 390 | self._cache[f"origin_token_{self.user_id}_{self.ident}"] = value 391 | 392 | @property 393 | def base_read_url(self) -> URL: 394 | return self._cache.get(f"base_read_url_{self.user_id}_{self.ident}", ) 395 | 396 | @base_read_url.setter 397 | def base_read_url(self, value: URL): 398 | self._cache[f"base_read_url_{self.user_id}_{self.ident}"] = value 399 | 400 | @property 401 | def user_id(self): 402 | return self.account_config.userShowId 403 | 404 | @property 405 | def pwd(self): 406 | ret = self.account_config.pwd 407 | if ret is None: 408 | ret = self.config_data.pwd 409 | return ret if ret is not None else "6666" 410 | 411 | @property 412 | def is_set_pwd(self): 413 | return self._cache.get(f"is_set_pwd_{self.user_id}_{self.ident}", False) 414 | 415 | 416 | if __name__ == '__main__': 417 | YMZV2() 418 | --------------------------------------------------------------------------------