Support get calendar

Co-authored-by: brian <brian@xtaolabs.com>
This commit is contained in:
brian 2023-03-15 17:31:36 +08:00 committed by GitHub
parent e2a43b1c5d
commit ca794ea619
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 184 additions and 19 deletions

View File

@ -15,22 +15,21 @@ class Client(Methods):
password: str = None, password: str = None,
cookie: str = None, cookie: str = None,
cookie_file_path: str = "cookie.txt", cookie_file_path: str = "cookie.txt",
client_vpn: bool = False,
): ):
self.username = username self.username = username
self.password = password self.password = password
self.cookie = cookie self.cookie = cookie
self.cookie_file_path = cookie_file_path self.cookie_file_path = cookie_file_path
if client_vpn:
self.host = "https://clientvpn.cqwu.edu.cn:10443/http/webvpn507e990968de07079b0f10d16c49bdb1cb8d3ca3a4d14f557999e92cbdf19fcd"
self.auth_host = self.host
else:
self.host = "http://ehall.cqwu.edu.cn" self.host = "http://ehall.cqwu.edu.cn"
self.auth_host = "http://authserver.cqwu.edu.cn" self.auth_host = "http://authserver.cqwu.edu.cn"
self.web_ehall_path = ""
self.cookies = Cookies() self.cookies = Cookies()
self.request = AsyncClient() self.request = AsyncClient()
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.me: Optional[User] = None self.me: Optional[User] = None
self._use_password_login = False
self.xue_nian = 2022 # 学年
self.xue_qi = 1 # 学期 0 为第一学期, 1 为第二学期
@staticmethod @staticmethod
def get_input(word: str = "", is_int: bool = False): def get_input(word: str = "", is_int: bool = False):

View File

@ -1,6 +1,7 @@
from .auth import Auth from .auth import Auth
from .epay import EPay from .epay import EPay
from .users import Users from .users import Users
from .webvpn import WebVPN
from .xg import XG from .xg import XG
@ -8,6 +9,7 @@ class Methods(
Auth, Auth,
EPay, EPay,
Users, Users,
WebVPN,
XG, XG,
): ):
pass pass

View File

@ -10,10 +10,12 @@ class LoginWithPassword:
self: "cqwu.Client", self: "cqwu.Client",
captcha_code: str = None, captcha_code: str = None,
show_qrcode: bool = True, show_qrcode: bool = True,
auth_host: str = None,
): ):
""" """
使用学号加密码登录 使用学号加密码登录
""" """
auth_host = auth_host or self.auth_host
headers = { headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Language': 'zh-CN,zh;q=0.9', 'Accept-Language': 'zh-CN,zh;q=0.9',
@ -22,7 +24,7 @@ class LoginWithPassword:
'Upgrade-Insecure-Requests': '1', 'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
} }
html = await self.request.get(f"{self.auth_host}/authserver/login", headers=headers, follow_redirects=True) html = await self.request.get(f"{auth_host}/authserver/login", headers=headers, follow_redirects=True)
self.cookies.update(html.cookies) self.cookies.update(html.cookies)
tree = etree.HTML(html.text) tree = etree.HTML(html.text)
pwd_default_encrypt_salt = tree.xpath('//*[@id="pwdDefaultEncryptSalt"]/@value')[0] pwd_default_encrypt_salt = tree.xpath('//*[@id="pwdDefaultEncryptSalt"]/@value')[0]
@ -46,18 +48,20 @@ class LoginWithPassword:
'Accept-Language': 'zh-CN,zh;q=0.9', 'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'max-age=0', 'Cache-Control': 'max-age=0',
'Connection': 'keep-alive', 'Connection': 'keep-alive',
'Origin': self.auth_host, 'Origin': auth_host,
'Referer': f'{self.auth_host}/authserver/login', 'Referer': f'{auth_host}/authserver/login',
'Upgrade-Insecure-Requests': '1', 'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63',
} }
html = await self.request.post( html = await self.request.post(
f"{self.auth_host}/authserver/login", f"{auth_host}/authserver/login",
headers=headers, headers=headers,
data=form_data, data=form_data,
follow_redirects=False, follow_redirects=False,
) )
if auth_host == self.auth_host:
if 'CASTGC' not in html.cookies.keys(): if 'CASTGC' not in html.cookies.keys():
raise UsernameOrPasswordError raise UsernameOrPasswordError
self.cookies.update(html.cookies) self.cookies.update(html.cookies)
self._use_password_login = True # noqa
self.me = await self.get_me() # noqa self.me = await self.get_me() # noqa

View File

@ -0,0 +1,22 @@
from httpx import URL
from .get_calendar import GetCalendar
from .get_calendar_change import GetCalendarChange
from .login_webvpn import LoginWebVPN
class WebVPN(
GetCalendar,
GetCalendarChange,
LoginWebVPN,
):
@staticmethod
def get_web_vpn_host(url: URL, https: bool = False) -> str:
return next(
(
f"https://clientvpn.cqwu.edu.cn/{'https' if https else 'http'}/{i}"
for i in str(url).split("/")
if i.startswith("webvpn")
),
None,
)

View File

@ -0,0 +1,42 @@
import base64
import cqwu
from cqwu.errors import CookieError
class GetCalendar:
async def get_calendar(
self: "cqwu.Client",
xue_nian: int = None,
xue_qi: int = None,
) -> str:
""" 获取课程表 """
xue_nian = xue_nian or self.xue_nian
xue_qi = xue_qi or self.xue_qi
jw_html = await self.request.get(
f"{self.web_ehall_path}/appShow?appId=5299144291521305", follow_redirects=True
)
if "教学管理服务平台" not in jw_html.text:
raise CookieError
jw_host = self.get_web_vpn_host(jw_html.url)
jw_url = f"{jw_host}/cqwljw/student/wsxk.xskcb10319.jsp"
params = {
"params": base64.b64encode(f"xn={xue_nian}&xq={xue_qi}".encode()).decode(),
}
headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Referer': f'{jw_host}/cqwljw/student/xkjg.wdkb.jsp?menucode=S20301',
'Sec-Fetch-Dest': 'iframe',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-User': '?1',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.41',
'sec-ch-ua': '"Microsoft Edge";v="111", "Not(A:Brand";v="8", "Chromium";v="111"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
}
jw_html = await self.request.get(jw_url, params=params, headers=headers, timeout=60, follow_redirects=True)
jw_html = jw_html.text.replace("""<script type="text/javascript" src="//clientvpn.cqwu.edu.cn/webvpn/bundle.debug.js" charset="utf-8"></script>""", "")
return jw_html.replace("<title></title>", '<meta charset="UTF-8">')

View File

@ -0,0 +1,73 @@
import cqwu
from cqwu.errors import CookieError
def get_in_middle(text: str, start: str, end: str) -> str:
return text.split(start, 1)[1].split(end, 1)[0]
class GetCalendarChange:
async def get_calendar_change(
self: "cqwu.Client",
xue_nian: int = None,
xue_qi: int = None,
) -> str:
""" 获取课程表 """
xue_nian = xue_nian or self.xue_nian
xue_qi = xue_qi or self.xue_qi
jw_html = await self.request.get(
f"{self.web_ehall_path}/appShow?appId=5299144291521305", follow_redirects=True
)
if "教学管理服务平台" not in jw_html.text:
raise CookieError
jw_host = self.get_web_vpn_host(jw_html.url, https=True)
jw_url = f"{jw_host}/cqwljw/student/jxap.jxaptzxx_rpt.jsp"
jw_sg_url = f"{jw_host}/cqwljw/STU_DynamicInitDataAction.do"
headers = {
'Accept': '*/*',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Origin': 'https://clientvpn.cqwu.edu.cn',
'Referer': f'{jw_host}/cqwljw/student/jxap.jxaptzxx.html?menucode=S20302',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.41',
'content-type': 'application/x-www-form-urlencoded',
'sec-ch-ua': '"Microsoft Edge";v="111", "Not(A:Brand";v="8", "Chromium";v="111"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
}
params = {
"classPath": "C73E288D0DEA8D7F772BBD7F8FDC7E66F44C9E3992261989ECBAC5A3D722B306C6354658E0F25121E24CED075326C19885F263F369E5CD668E2EEE7CFB7EB5788F202FC6FD7DB0C96FB6995C1DD96ADE84BE3E72CFFBE9EC74FA044498BD2D21EA0439F9DC625F0EF61B7159924C542D577F814848F27128"
}
res = await self.request.post(jw_sg_url, params=params, headers=headers, timeout=60, follow_redirects=True)
data = {
'xh': get_in_middle(res.text, '<xh>', '</xh>'),
'xn': str(xue_nian),
'xq': str(xue_qi),
'xnxq': f'{xue_nian},{xue_qi}',
'menucode_current': 'S20302',
}
headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'max-age=0',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded',
'Origin': 'https://clientvpn.cqwu.edu.cn',
'Referer': f'{jw_host}/cqwljw/student/jxap.jxaptzxx.html?menucode=S20302',
'Sec-Fetch-Dest': 'iframe',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-User': '?1',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.41',
'sec-ch-ua': '"Microsoft Edge";v="111", "Not(A:Brand";v="8", "Chromium";v="111"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
}
jw_html = await self.request.post(jw_url, data=data, headers=headers, timeout=60, follow_redirects=True)
jw_html = jw_html.text.replace("""<script type="text/javascript" src="//clientvpn.cqwu.edu.cn/webvpn/bundle.debug.js" charset="utf-8"></script>""", "")
jw_html = jw_html.replace("""<script language='javascript' type='text/javascript' src='../js/Print.js'></script>""", "")
return jw_html.replace("<title></title>", '<meta charset="UTF-8">')

View File

@ -0,0 +1,21 @@
import cqwu
class LoginWebVPN:
async def login_web_vpn(
self: "cqwu.Client",
):
"""
登录 WebVPN
"""
if not self._use_password_login:
return
url = "https://webvpn.cqwu.edu.cn"
ehall_html = await self.request.get(url, follow_redirects=True)
self.web_ehall_path = self.get_web_vpn_host(ehall_html.url) # noqa
await self.oauth("http://authserver.cqwu.edu.cn/authserver/login?service=https://clientvpn.cqwu.edu.cn/enlink/api/client/callback/cas")
auth_html = await self.request.get(
f"{self.web_ehall_path}/login", follow_redirects=True
)
if web_auth_path := self.get_web_vpn_host(auth_html.url):
await self.login_with_password(auth_host=web_auth_path)

View File

@ -9,8 +9,8 @@ from cqwu.errors.auth import CookieError
class GetScore: class GetScore:
async def get_score( async def get_score(
self: "cqwu.Client", self: "cqwu.Client",
year: int = 2022, year: int = None,
semester: int = 1, semester: int = None,
) -> List["types.Score"]: ) -> List["types.Score"]:
""" """
获取期末成绩 获取期末成绩
@ -18,6 +18,8 @@ class GetScore:
Returns: Returns:
List[types.Score]: 成绩列表 List[types.Score]: 成绩列表
""" """
year = year or self.xue_nian
semester = semester or self.xue_qi
url = "http://xg.cqwu.edu.cn/xsfw/sys/zhcptybbapp/*default/index.do#/cjcx" url = "http://xg.cqwu.edu.cn/xsfw/sys/zhcptybbapp/*default/index.do#/cjcx"
html = await self.oauth(url) html = await self.oauth(url)
if not html: if not html:

View File

@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
setuptools.setup( setuptools.setup(
name="cqwu", # 用自己的名替换其中的YOUR_USERNAME_ name="cqwu", # 用自己的名替换其中的YOUR_USERNAME_
version="0.0.3", # 包版本号,便于维护版本 version="0.0.4", # 包版本号,便于维护版本
author="omg-xtao", # 作者,可以写自己的姓名 author="omg-xtao", # 作者,可以写自己的姓名
author_email="xtao@xtaolink.cn", # 作者联系方式,可写自己的邮箱地址 author_email="xtao@xtaolink.cn", # 作者联系方式,可写自己的邮箱地址
description="A cqwu ehall client.", # 包的简述 description="A cqwu ehall client.", # 包的简述
@ -18,7 +18,7 @@ setuptools.setup(
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
], ],
python_requires='>=3.6', # 对python的最低版本要求 python_requires='>=3.8', # 对python的最低版本要求
install_requires=[ install_requires=[
"httpx", "httpx",
"lxml", "lxml",