├── 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 |
--------------------------------------------------------------------------------