diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1edfe46..98cb1c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Continuous Integration +name: Continuous Integration And Automated Testing on: push: @@ -111,7 +111,7 @@ jobs: artifact/* - name: Publish package - if: false #(! endsWith(github.ref, 'dev')) + if: (! endsWith(github.ref, 'dev')) uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 with: user: __token__ diff --git a/README.md b/README.md index 0f22aba..ed141a0 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,58 @@ -# HustPass -用于验证 HustPass@2023 的 python-lib - -此仓库现独立维护 - -[README(EN ver)](READ_EN.md) +# HustLogin +A python-lib for authenticating HustPass@2023 ![HustPassLogo](https://pass.hust.edu.cn/cas/comm/image/logo-inside.png) -b站教程(来自上游) [Bilibili](https://www.bilibili.com/video/BV1bX4y1j7vR/) - -Python-requests依赖注入模式(来自上游) [HustAuth](https://github.com/MarvinTerry/HustAuth) +> Faster, Easier, Lighter -> 更快、更简单、更方便 +Attention: HustPass login protocol underwent a major update on 2023/05/23, moving from DES to RSA, previous login libraries are now deprecated. -注意:HustPass登录协议于2023年5月23日进行了重大更新,从DES迁移到RSA,以前的登录库现已不可使用。 +Tutorials on [Bilibili](https://www.bilibili.com/video/BV1bX4y1j7vR/) -## 安装 +Plug-in for python-requests [HustAuth](https://github.com/MarvinTerry/HustAuth) -该库已发布在 PyPI 上 **[hust_login](https://pypi.org/project/hust-login/)** +## Installing -通过单行命令安装,pip将自动处理依赖。 +The library has been made publicly available on PyPI **[hust_login](https://pypi.org/project/hust-login/)** +Installing by a single line of command, and requirements will be automatically handled. ``` pip install hust_login ``` +Additionally, you need to install ```tesseract-ocr``` back end: -此外,您需要安装```tesseract-ocr```后端: +- Win: [download binary here](https://tesseract-ocr.github.io/tessdoc/Downloads.html), "3rd party Windows exe’s/installer" recommanded. +- Linux: run ```sudo apt install tesseract-ocr``` -- Win:[在此处下载二进制文件](https://tesseract-ocr.github.io/tessdoc/Downloads.html),推荐“3rd party Windows exe’s/installer”。 -- Linux:运行```sudo apt install tesseract-ocr```。推荐使用appimage版本 +## Documentation +### **```hust_login.HustLogin(username, password, headers=None)```** -## 文档 -### **```hust_login.HustLogin(用户名,密码,标头(可选)```** + PARAMETERS: + - username -- Username of pass.hust.edu.cn e.g. U2022XXXXX + - password -- Password of pass.hust.edu.cn + - headers -- Headers you want to use, optional (the default header works fine) - 参数: - - 用户名 -- pass.hust.edu.cn 的用户名 例如 U2022XXXXX - - 密码 -- pass.hust.edu.cn 的密码 - - 标头 -- 您希望使用的标头,可选 + RETURNS: + - A **```requests.Session```** object that is already logged in + - use it the same way you use requests, e.g. + ```python + s = hust_login.HustPass('U2022XXXXX','YOUR-PASSWORD') + ret = s.get(your_url) + print(ret.text) + ``` +### **```hust_login.HustLogin(username, password, headers=None)```** - 返回: - - 已登录的 **```requests.Session```** 对象 - - 使用它的方式与使用请求的方式相同,例如 - ```python - s = hust_login.HustPass('U2022XXXXX','您的密码') - ret = s.get(your_url) - print(ret.text) - ``` -### **```hust_login.HustPass(用户名,密码,标头(可选))```** + PARAMETERS: Same as HustLogin - 参数:与HustLogin相同 + RETURNS: + - A class that contains wrapped common functions like QueryElectricityBills, QueryCurriculum, QueryFreeRoom, etc. - 返回: - - 一个类,包含QueryElectricityBills,QueryCurriculum,QueryFreeRoom等已包装的常用函数。 +> BE CREATIVE!!! ## Demo -演示如何查询考试成绩 - +Demonstrating how to query the exam result - CODE: ```python from hust_login import HustPass @@ -70,19 +65,17 @@ pip install hust_login for col in row.contents: print(col.text.strip(), end=" ") print("") - - ``` - **建议**在 ```with``` 语句中调用 ```HustPass```,如图所示。 + ``` + It's **recommended** to call ```HustPass``` in the ```with``` statement, as shown. - RESULT: - ``` - setting up session... - captcha detected, trying to decaptcha... - decaptching... - encrypting u/p... - captcha_code:0344 - posting login-form... - ---HustPass Succeed--- - + ``` + setting up session... + captcha detected, trying to decaptcha... + decaptching... + encrypting u/p... + captcha_code:4608 + posting login-form... + ---HustPass Succeed--- 课程名称 课程学分 课程成绩 备注 微积分(一)(上) 5.5 90 综合英语(一) 3.5 94 @@ -97,29 +90,32 @@ pip install hust_login 必修课总学分 50.50 公选课总学分 2.00 总学分 52.5 - ``` + ``` -## 开发 +## Development -如果该库不再有效,请尝试发出pr以使该库再次工作! +If the lib outdated, try to make a pull request to get this lib working again! -在常规登录期间启用加密和发布登录表单的 js 脚本是公开可用的 [login_standar.js?v=20230523](https://pass.hust.edu.cn/cas/comm/js/login_standar.js?v=20230523)。 我们的工作是将js翻译成python并处理验证码。(并提供一系列常用查询函数) +The js-scripts that enable encrypting and posting the login-form during regular login are publicly available [login_standar.js?v=20230523](https://pass.hust.edu.cn/cas/comm/js/login_standar.js?v=20230523). My job was to translate the js into python and deal with the captcha code. -如果您正在开发类似的库或该库的较新版本,以下是值得一提的内容: +Here are something worth mentioning if you are developing a newer version of the lib: -- RSA加密: - - PublicKey采用base64编码,请先解码。 - - 您加密的usr/pass应该以base64编码,并转换为文本而不是字节。您可以查看我的代码,看看它是如何工作的。 -- 解码器 - - 使用```BytesIO``` 方法将包含 gif 的字节流转换为文件。 - - 采用 Genius 方法对 4 帧 gif 进行组合和去噪:据观察,**数量**像素至少会出现在 3 帧中,而**噪声**像素小于 2。这提供了一种准确的方法来对图片进行去噪。 这是代码片段: - ```python - img_merge = Image.new(mode='L',size=(width,height),color=255) - for pos in [(x,y) for x in range(width) for y in range(height)]: - if sum([img.getpixel(pos) < 254 for img in img_list]) >= 3: +- Encrytion: + - PublicKey is encoded in base64, decode it first. + - The usr/pass you encrypted should be encoded in base64, and converted into the text instead of bytes. Look deeper into my code to see how it works. +- Decaptcha + - The ```BytesIO``` method is used to convert byte-stream containing the gif into the file. + - Genius approach applied to combine and de-noise the 4-frame gif: As observed, the **number** pixels would appear in 3 frames at least, while **noise** pixels are less than 2. This provides a super accurate way to de-noise the picture. Here's the code clip, try to understand: + ```python + img_merge = Image.new(mode='L',size=(width,height),color=255) + for pos in [(x,y) for x in range(width) for y in range(height)]: + if sum([img.getpixel(pos) < 254 for img in img_list]) >= 3: img_merge.putpixel(pos,0) - ``` - ![org](images/captcha_code.gif) ![processed](images/captcha_code_processed.png) -- 网络: - - 一个常见的假UA是必不可少的! HustPass 已阻止 python-requests 的默认UA。 + ``` + ![org](images/captcha_code.gif) ![processed](images/captcha_code_processed.png) + +- Network + - A common fake User-Agent is essential! HustPass has blocked python-requests's default User-Agent. + + diff --git a/README_EN.md b/README_EN.md deleted file mode 100644 index f059e8c..0000000 --- a/README_EN.md +++ /dev/null @@ -1,127 +0,0 @@ -# HustLogin -A python-lib for authenticating HustPass@2023 - -![HustPassLogo](https://pass.hust.edu.cn/cas/comm/image/logo-inside.png) - -[README(中文版)](READ.md) - -Video Tutorial(from upstream) [Bilibili](https://www.bilibili.com/video/BV1bX4y1j7vR/) - -Python-requests DI(from upstream) [HustAuth](https://github.com/MarvinTerry/HustAuth) - -> Faster, Easier, Lighter - -Attention: HustPass login protocol underwent a major update on 2023/05/23, moving from DES to RSA, previous login libraries are now deprecated. - -Tutorials on [Bilibili](https://www.bilibili.com/video/BV1bX4y1j7vR/) - -Plug-in for python-requests [HustAuth](https://github.com/MarvinTerry/HustAuth) - -## Installing - -The library has been made publicly available on PyPI **[hust_login](https://pypi.org/project/hust-login/)** - -Installing by a single line of command, and requirements will be automatically handled. - -``` -pip install hust_login -``` - -Additionally, you need to install ```tesseract-ocr``` back end: - -- Win: [download binary here](https://tesseract-ocr.github.io/tessdoc/Downloads.html), "3rd party Windows exe’s/installer" recommanded. -- Linux: run ```sudo apt install tesseract-ocr```. It's recommended to use the appimage version - -## Documentation -### **```hust_login.HustLogin(username, password, headers=None)```** - - PARAMETERS: - - username -- Username of pass.hust.edu.cn e.g. U2022XXXXX - - password -- Password of pass.hust.edu.cn - - headers -- Headers you want to use, optional (the default header works fine) - - RETURNS: - - A **```requests.Session```** object that is already logged in - - use it the same way you use requests, e.g. - ```python - s = hust_login.HustPass('U2022XXXXX','YOUR-PASSWORD') - ret = s.get(your_url) - print(ret.text) - ``` -### **```hust_login.HustLogin(username, password, headers=None)```** - - PARAMETERS: Same as HustLogin - - RETURNS: - - A class that contains wrapped common functions like QueryElectricityBills, QueryCurriculum, QueryFreeRoom, etc. - -> BE CREATIVE!!! - -## Demo -Demonstrating how to query the exam result -- CODE: - ```python - from hust_login import HustPass - from bs4 import BeautifulSoup - - with HustPass('U2022XXXXX','YOUR-PASSWORD') as s: - ret = s.get('http://hub.m.hust.edu.cn/cj/cjsearch/findcjinfo.action?xn=2022&xq=0') - soup = BeautifulSoup(ret.content, 'html.parser') - for row in soup.find_all('tr'): - for col in row.contents: - print(col.text.strip(), end=" ") - print("") - ``` - It's **recommended** to call ```HustPass``` in the ```with``` statement, as shown. -- RESULT: - ``` - setting up session... - captcha detected, trying to decaptcha... - decaptching... - encrypting u/p... - captcha_code:4608 - posting login-form... - ---HustPass Succeed--- - 课程名称 课程学分 课程成绩 备注 - 微积分(一)(上) 5.5 90 - 综合英语(一) 3.5 94 - 线性代数 2.5 92 - 工程制图(一) 2.5 98 - 综合英语(二) 3.5 93 - 微积分(一)(下) 5.5 94 - ... - ... - ... - 加权排名成绩 91.71 - 必修课总学分 50.50 - 公选课总学分 2.00 - 总学分 52.5 - ``` - -## Development - -If the lib outdated, try to make a pull request to get this lib working again! - -The js-scripts that enable encrypting and posting the login-form during regular login are publicly available [login_standar.js?v=20230523](https://pass.hust.edu.cn/cas/comm/js/login_standar.js?v=20230523). My job was to translate the js into python and deal with the captcha code. - -Here are something worth mentioning if you are developing a newer version of the lib: - -- Encrytion: - - PublicKey is encoded in base64, decode it first. - - The usr/pass you encrypted should be encoded in base64, and converted into the text instead of bytes. Look deeper into my code to see how it works. -- Decaptcha - - The ```BytesIO``` method is used to convert byte-stream containing the gif into the file. - - Genius approach applied to combine and de-noise the 4-frame gif: As observed, the **number** pixels would appear in 3 frames at least, while **noise** pixels are less than 2. This provides a super accurate way to de-noise the picture. Here's the code clip, try to understand: - ```python - img_merge = Image.new(mode='L',size=(width,height),color=255) - for pos in [(x,y) for x in range(width) for y in range(height)]: - if sum([img.getpixel(pos) < 254 for img in img_list]) >= 3: - img_merge.putpixel(pos,0) - ``` - ![org](images/captcha_code.gif) ![processed](images/captcha_code_processed.png) - -- Network - - A common fake User-Agent is essential! HustPass has blocked python-requests's default User-Agent. - - - diff --git a/hust_login/_HustPass.py b/hust_login/_HustPass.py index 0a68626..0553eb1 100644 --- a/hust_login/_HustPass.py +++ b/hust_login/_HustPass.py @@ -1,5 +1,9 @@ from .login import HustLogin, CheckLoginStatu -from .query import * +from .utility_bills import GetElectricityBill +from .curriculum import QuerySchedules +from .free_room import GetFreeRooms +from .ecard_bills import GetEcardBills +from .curriculum_physic import GetPhysicsLab class HustPass_NotLoged(BaseException): def __init__(self, *args: object) -> None: diff --git a/hust_login/__init__.py b/hust_login/__init__.py index b19879b..8731095 100644 --- a/hust_login/__init__.py +++ b/hust_login/__init__.py @@ -1,5 +1,8 @@ from .login import HustLogin, CheckLoginStatu -from ._HustPass import HustPass, HustPass_NotLoged +from . import curriculum # 课表相关归属在此命名空间下 +from ._HustPass import HustPass +from . import free_room +from . import utility_bills as bills from importlib.metadata import version, PackageNotFoundError diff --git a/hust_login/__main__.py b/hust_login/__main__.py index c0bcd80..696f780 100644 --- a/hust_login/__main__.py +++ b/hust_login/__main__.py @@ -3,25 +3,27 @@ import json from getopt import getopt from . import HustPass -from .cli import show_usage,tasker,cli +from ._cli import _show_usage,_tasker,cli import logging def main(): try: - opts, args = getopt(sys.argv[1:],'U:P:f:o:hvi',['help','version','inputformat','debug','interactive']) + opts, args = getopt(sys.argv[1:],'U:P:f:o:hvi',['autotest','help','version','inputformat','debug','interactive']) except: - return show_usage() + return _show_usage() + autotest = False log_level = logging.INFO fpath = None opath = None - Is_interactive = False for opt,arg in opts: if opt in ['-h', '--help']: - return show_usage(0) + return _show_usage(0) elif opt in ['-v', '--version']: from . import __version__ return __version__ + elif opt == '--autotest': + autotest = True elif opt == '--inputformat': with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'example.json'),'r') as fp: print(fp.read()) @@ -39,28 +41,25 @@ def main(): elif opt in ['-i','--interactive']: return cli() else: - return show_usage() + return _show_usage() + logging.basicConfig(level=log_level,\ format='[%(levelname)s] %(message)s') - if Is_interactive: - if not Uid and not Pwd: - return cli((Uid, Pwd)) - return cli() header = None if fpath is not None: with open(fpath,'r') as fp: try: conf = json.loads(fp.read()) except FileNotFoundError: - return show_usage(-2) + return _show_usage(-2) except json.decoder.JSONDecodeError: - return show_usage(-3) + return _show_usage(-3) try: Uid = conf['Uid'] Pwd = conf['Pwd'] except KeyError: - return show_usage(-3) + return _show_usage(-3) try: header = conf['Headers'] except KeyError: @@ -69,20 +68,27 @@ def main(): try: HUSTpass = HustPass(Uid, Pwd, header) except NameError: - return show_usage() + return _show_usage() except ConnectionRefusedError: print('HUSTPASS: Authentication failed') return -1 - + if autotest: + from .autotest import full_test + code = full_test(HUSTpass) + if code != 0: + print('Test Failed') + return code + return 0 + if opath is not None: try: with open(opath,'w') as fp: - fp.write(json.dumps(tasker(HUSTpass, conf['Tasks']))) + fp.write(json.dumps(_tasker(HUSTpass, conf['Tasks']))) except: return -1 else: - print(tasker(HUSTpass, conf['Tasks'])) + print(_tasker(HUSTpass, conf['Tasks'])) return 0 if __name__ == '__main__': diff --git a/hust_login/cli/__init__.py b/hust_login/cli/__init__.py deleted file mode 100644 index 8da779b..0000000 --- a/hust_login/cli/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .utils import tasker,show_usage -from ._cli import cli \ No newline at end of file diff --git a/hust_login/cli/_cli.py b/hust_login/cli/_cli.py deleted file mode 100644 index 3af602d..0000000 --- a/hust_login/cli/_cli.py +++ /dev/null @@ -1,142 +0,0 @@ - -def cli(auth=None): - - answer = prompt(interface[0]) - if answer['init'] == 'Exit': - return 0 - - while 1: - if auth == None: - auth = prompt(login_para[:2]) - try: - from .. import HustPass - HUSTpass = HustPass(auth['UID'], auth['PWD']) - break - except NameError: - answer = prompt(login_para[3]) - if not answer['IsExit']: - return -1 - except ConnectionRefusedError: - print('HUSTPASS: Authentication failed') - return -1 - except ValueError as e: - print(e) - auth = None - - while 1: - answer = prompt(interface[1]) - if answer['actions'] == 'Exit': - break - try: - if answer['actions'] == 'Bills': - answer = prompt(interface[2]) - if answer['bills'] == 'Go Back': - continue - date = get_date() - if answer['bills'] == 'E-card': - result = HUSTpass.QueryEcardBills(date) - elif answer['bills'] == 'Dormitory': - result = HUSTpass.QueryElectricityBills(date) - elif answer['actions'] == 'Curriculum': - date = get_date() - result = HUSTpass.QuerySchedules(date) - elif answer['actions'] == 'Rooms': - date = get_date() - result = HUSTpass.QueryFreeRooms(date) - - print(result) - except: - print('!!!Error Happened') - - print('Cleaning...') # Useless, just for Experience - return 0 - -def prompt(arg): - from PyInquirer import prompt as _prompt#, Separator - ret = _prompt(arg) - if not len(ret): - raise Exception('Do not click, use Enter') - return ret - -def get_quick_date() -> tuple['Today', 'Tomorrow', 'Yesterday']: #type:ignore - from datetime import datetime, timedelta - _today = datetime.now() - _delta = timedelta(days=1) - return {'Today':_today.date().isoformat(),'Tomorrow':(_today+_delta).date().isoformat(),'Yesterday': (_today-_delta).date().isoformat()} - -def get_date() -> str: - from PyInquirer import prompt - answer = prompt(login_para[4]) - if answer['DATE'] == 'Type': - answer = prompt(login_para[2]) - else: - answer['DATE'] = get_quick_date()[answer['DATE']] - return answer['DATE'] - -interface = [ - { - 'type': 'list', - 'name': 'init', - 'message': 'What do you want to do?', - 'choices': [ - 'Login', - 'Exit' - ] - }, - { - 'type':'list', - 'name':'actions', - 'message':'What you want to do?', - 'choices':[ - 'Bills', - 'Curriculum', - 'Rooms', - 'Exit' - ] - }, - { - 'type':'list', - 'name':'bills', - 'message':'Which your want?', - 'choices':[ - 'Dormitory', - 'E-card', - 'Go Back' - ] - } -] -login_para = [ - { - 'type': 'input', - 'name': 'UID', - 'message': 'Your Uid:', - # 'validate': PhoneNumberValidator - }, - { - 'type': 'password', - 'name': 'PWD', - 'message': 'Your Pwd:' - }, - { - 'type': 'input', - 'name': 'DATE', - 'message': 'Which time? [format:1970-01-01]' - }, - { - 'type': 'confirm', - 'name': 'IsExit', - 'message': 'Wrong uid,pwd, try again?', - 'default': 'Ture' - }, - { - 'type':'list', - 'name':'DATE', - 'message':'Which time?', - 'choices':[ - 'Today', - 'Tomorrow', - 'Yesterday', - 'Type' - ] - } -] diff --git a/hust_login/cli/utils.py b/hust_login/cli/utils.py deleted file mode 100644 index c7bb8ef..0000000 --- a/hust_login/cli/utils.py +++ /dev/null @@ -1,51 +0,0 @@ - -def __get_result(Dict:dict, Func) -> dict: - ret = {} - for key, value in Dict.items(): - if key == 'list': - ret[key] = Func(value) - elif key == 'str': - ret[key] = Func(value) - elif key == 'tuple': - ret[key] = Func((value[0],value[1])) - elif key == 'int': - ret[key] = Func(value) - else: - raise KeyError('UNEXPECTED PARAMETERS') - return ret - -def show_usage(code:int=-1): - print('''\nUSAGE: python -m hust_pass