版本 2.0

This commit is contained in:
pooneyy 2023-09-19 16:00:59 +08:00
parent 043cbfcb9d
commit 9040dd3065
No known key found for this signature in database
6 changed files with 370 additions and 105 deletions

6
.gitignore vendored
View File

@ -1,5 +1,7 @@
__pycache__
build
dist
__pycache__
env
*.bat
*.json
*.spec

View File

@ -1,53 +1,39 @@
# weiban-tool
<h1 align="center">安全微伴自动刷课助手</h1>
<p align="center" class="shields">
<img src="https://badges.toozhao.com/badges/01HAMCFS652W02Z5H3CE02M4JY/blue.svg" alt="Visitors"/>
</p>
安全微伴自动刷课助手
相关项目:[安全微伴题库](https://github.com/pooneyy/WeibanQuestionsBank) | 安全微伴自动刷课助手
[原项目](https://github.com/Coaixy/weiban-tool)作者已停止维护,我在原项目基础上增加多账号的支持
### 项目介绍
### 使用方法
安全微伴自动刷课助手(多账号版),脱胎于[Coaixy/weiban-tool](https://github.com/Coaixy/weiban-tool),在原项目基础上增加多账号的支持,可以同时进行多个账号的学习任务。
1. 登录[安全微伴 (mycourse.cn)](http://weiban.mycourse.cn/#/login)。
### 使用说明
2. 在浏览器地址栏运行
1. 运行`main.py` 或者 [main.exe](https://github.com/pooneyy/weiban-tool/releases)。
```javascript
javascript:(function(){data=JSON.parse(localStorage.user);prompt('',JSON.stringify({token:data['token'],userId:data['userId'], tenantCode:data['tenantCode'], userProjectId: data['preUserProjectId'], realName: data['realName']}));})();
```
2. **支持验证码识别**,验证码识别使用[TrueCaptcha](https://truecaptcha.org/),会提示你输入`userid`和`apikey`,注册的方法此处不过多赘述。
浏览器地址栏如果吞掉了“`javascript:`”,请手动加上
需要提醒的是这是一个付费服务每个账号每天享有30次免费识别服务每个账号总共享有100次免费识别服务。
或者你可以将上述脚本[添加到收藏夹](https://www.qiuyelin.com/getWei-banToken.html),直接在登录后的页面上运行添加进收藏夹的脚本
关于资费1美元可以识别3000次。可以使用PayPal国区支付关于汇率2023年9月18日使用PayPal$1USD=¥7.56CNY
3. 复制弹窗内的内容,**按照格式**添加到`config.json`。(格式不对会报错)
**值得一提的是,你可以跳过这一步骤,登录时将手动输入验证码。**
[![1662441411827.png](http://png.eot.ooo/i/2022/09/06/6316d7c7f3567.png)](http://png.eot.ooo/i/2022/09/06/6316d7c7f3567.png)
3. 按照提示录入账号密码,可同时依次输入多个账号,会记录上一个账号的学校名称,当有多个账号来自同一个学校,可以不用重复输入学校名。
4. 以`UTF-8`的编码方式创建`config.json`文件。其内容格式如下:
4. 按`Ctrl`+`C`结束录入账号,开始登录,如果在第二步没有输入`userid`和`apikey`,会提示输入验证码。
> `config.json`
>
> ```json
> [
> {"token":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","userId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","tenantCode":"00000001","userProjectId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"},
> {"token":"yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy","userId":"yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy","tenantCode":"00000002","userProjectId":"yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"},
> {"token":"zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz","userId":"zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz","tenantCode":"00000003","userProjectId":"zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz"},
> {#第4个账号信息#},
> {#第5个账号信息#},
> ...
> {#第n个账号信息#}
> ]
> ```
5. 运行`main.py` 或者 [main.exe](https://github.com/pooneyy/weiban-tool/releases)。
![](https://telegraph-image1.pages.dev/file/e46f287b9733d3b8d21bc.png)
### 更新日志
```text
版本 1.1 at 2022-09-06 15:08:08
优化:增加对多账户的支持。
版本 1.2 at 2022-09-07 14:02:39
优化使用异步函数提高多账户场景下任务执行效率避免由于多个账户排队时任务流程过长Token过期导致后面的账户任务失败。
优化:使显示内容更简洁。
```
- 版本 1.1 at 2022-09-06 15:08:08
- 优化:增加对多账户的支持。
- 版本 1.2 at 2022-09-07 14:02:39
- 优化使用异步函数提高多账户场景下任务执行效率避免由于多个账户排队时任务流程过长Token过期导致后面的账户任务失败。
- 优化:使显示内容更简洁。
- 版本 2.0 at 2023-09-18 21:57:16
- 优化:使用账号密码登录,登录相关的代码来自[Coaixy/weiban-tool/enco.py](https://github.com/Coaixy/weiban-tool/blob/bf08fe823953afa834b49fe8d7e7a1d5abf7e605/enco.py)。

343
Utils.py
View File

@ -1,26 +1,68 @@
import time
import requests
import json
import asyncio
import datetime
import json
import os
from PIL import Image
import random
import requests
import time
# From https://github.com/JefferyHcool/weibanbot/blob/main/enco.py
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad
import base64
DEFAULT_SCHOOL_NAME = ''
'''这个常量的作用是暂存学校名,当同时输入的多个帐号来自同一个学校,用此避免重复地输入学校名'''
class main:
tenantCode = 0
userId = ""
x_token = ""
userProjectId = ""
realName = ""
taskName = ""
resourceNames = ['第0项']
headers = {'x-token': "",
"User-agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134 Mobile Safari/537.36 Edg/103.0.1264.77"
}
def __init__(self, code, id, token,projectId):
def __init__(self, code, id, token, realName):
self.tenantCode = code
self.userId = id
self.x_token = token
self.userProjectId = projectId
self.realName = realName
def init(self):
self.headers['x-token'] = self.x_token
# 以下俩个方法来自https://github.com/Sustech-yx/WeiBanCourseMaster
# js里的时间戳似乎都是保留了三位小数的.
def __get_timestamp(self):
return str(round(datetime.datetime.now().timestamp(), 3))
# Magic: 用于构造、拼接"完成学习任务"的url
# js: (jQuery-3.2.1.min.js)
# f = '3.4.1'
# expando = 'jQuery' + (f + Math.random()).replace(/\D/g, "")
def __gen_rand(self):
return ("3.4.1" + str(random.random())).replace(".", "")
def get_Project_Info(self):
url = f'https://weiban.mycourse.cn/pharos/index/listMyProject.do?timestamp={time.time()}'
data = {
'tenantCode': self.tenantCode,
'userId': self.userId,
'ended': 2
}
response = requests.post(url, data=data, headers=self.headers)
data = json.loads(response.text)['data']
if len(data) <= 0:self.userProjectId = ''
else:
self.userProjectId = data[0]["userProjectId"]
self.taskName = data[0]["projectName"]
def getRealName(self):
url = f"https://weiban.mycourse.cn/pharos/my/getInfo.do?timestamp={int(time.time())}"
data = {
@ -58,13 +100,13 @@ class main:
data = json.loads(text)
return data['data']['progressPet']
def getCategory(self):
def getCategory(self, chooseType):
url = "https://weiban.mycourse.cn/pharos/usercourse/listCategory.do"
data = {
'userProjectId': self.userProjectId,
'tenantCode': self.tenantCode,
'userId': self.userId,
'chooseType': 3
'chooseType': chooseType
}
response = requests.post(url, data=data, headers=self.headers)
text = response.text
@ -76,69 +118,278 @@ class main:
result.append(i['categoryCode'])
return result
def getCourse(self):
def getCourse(self, chooseType):
url = "https://weiban.mycourse.cn/pharos/usercourse/listCourse.do"
result = []
for i in self.getCategory():
for i in self.getCategory(chooseType):
data = {
'userProjectId': self.userProjectId,
'tenantCode': self.tenantCode,
'userId': self.userId,
'chooseType': 3,
'name': "",
'categoryCode': i
"userProjectId": self.userProjectId,
"tenantCode": self.tenantCode,
"userId": self.userId,
"chooseType": chooseType,
"name": "",
"categoryCode": i,
}
response = requests.post(url, data=data, headers=self.headers)
text = response.text
data = json.loads(text)['data']
data = json.loads(text)["data"]
for i in data:
if i['finished'] == 2:
result.append(i['resourceId'])
if i["finished"] == 2:
result.append(i["resourceId"])
return result
def getFinishIdList(self):
def getFinishIdList(self, chooseType):
url = "https://weiban.mycourse.cn/pharos/usercourse/listCourse.do"
result = {}
for i in self.getCategory():
for i in self.getCategory(chooseType):
data = {
'userProjectId': self.userProjectId,
'tenantCode': self.tenantCode,
'userId': self.userId,
'chooseType': 3,
'name': "",
'categoryCode': i
"userProjectId": self.userProjectId,
"tenantCode": self.tenantCode,
"userId": self.userId,
"chooseType": chooseType,
"categoryCode": i,
}
response = requests.post(url, data=data, headers=self.headers)
text = response.text
data = json.loads(text)['data']
data = json.loads(text)["data"]
for i in data:
if i['finished'] == 2:
result[i['resourceId']] = i['userCourseId']
if i["finished"] == 2:
if "userCourseId" in i:
result[i["resourceId"]] = i["userCourseId"]
# print(i['resourceName'])
self.resourceNames.append(i['resourceName'])
self.tempUserCourseId = i["userCourseId"]
else:
result[i["resourceId"]] = self.tempUserCourseId
return result
async def start(self,courseId):
data = {
'userProjectId': self.userProjectId,
'tenantCode': self.tenantCode,
'userId': self.userId,
'courseId': courseId
"userProjectId": self.userProjectId,
"tenantCode": self.tenantCode,
"userId": self.userId,
"courseId": courseId,
}
headers = {
"x-token":self.x_token
}
res = requests.post("https://weiban.mycourse.cn/pharos/usercourse/study.do",data=data,headers=headers)
headers = {"x-token": self.x_token}
res = requests.post(
"https://weiban.mycourse.cn/pharos/usercourse/study.do",
data=data,
headers=headers,
)
while json.loads(res.text)['code'] == -1:
await asyncio.sleep(5)
res = requests.post("https://weiban.mycourse.cn/pharos/usercourse/study.do",data=data,headers=headers)
res = requests.post(
"https://weiban.mycourse.cn/pharos/usercourse/study.do",
data=data,
headers=headers,
)
print(f"start:{courseId}\r",end='')
def finish(self,finishId):
params = {
"callback":"",
"userCourseId":finishId,
"tenantCode":self.tenantCode
def finish(self, courseId, finishId):
get_url_url = "https://weiban.mycourse.cn/pharos/usercourse/getCourseUrl.do"
finish_url = "https://weiban.mycourse.cn/pharos/usercourse/v1/{}.do"
data = {
"userProjectId": self.userProjectId,
"tenantCode": self.tenantCode,
"userId": self.userId,
"courseId": courseId,
}
url = "https://weiban.mycourse.cn/pharos/usercourse/finish.do"
requests.get(url=url,params=params)
print(f"finish:{finishId}\r",end='')
raw_data = requests.post(get_url_url, data=data, headers=self.headers)
url = json.loads(raw_data.text.encode().decode("unicode-escape"))["data"]
token = url[url.find("methodToken="): url.find("&csCom")].replace(
"methodToken=", ""
)
# print(token)
finish_url = finish_url.format(token)
ts = self.__get_timestamp().replace(".", "")
param = {
"callback": "jQuery{}_{}".format(self.__gen_rand(), ts),
"userCourseId": finishId,
"tenantCode": self.tenantCode,
"_": str(int(ts) + 1),
}
requests.get(finish_url, params=param, headers=self.headers).text
print(f"{self.realName} Finish:{courseId}")
def fill_key(key):
key_size = 128
filled_key = key.ljust(key_size // 8, b'\x00')
return filled_key
def aes_encrypt(data, key):
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(data.encode('utf-8'), AES.block_size))
base64_cipher = base64.b64encode(ciphertext).decode('utf-8')
result_cipher = base64_cipher.replace('+', '-').replace('/', '_')
return result_cipher
def login(payload):
init_key = 'xie2gg'
key = fill_key(init_key.encode('utf-8'))
encrypted = aes_encrypt(
f'{{"keyNumber":"{payload["userName"]}","password":"{payload["password"]}","tenantCode":"{payload["tenantCode"]}","time":{payload["timestamp"]},"verifyCode":"{payload["verificationCode"]}"}}',
key
)
return encrypted
def apitruecaptcha(config, content):
image=base64.b64encode(content)
url = 'https://api.apitruecaptcha.org/one/gettext'
data = {
'data':str(image,'utf-8'),
'userid':config["TrueCaptcha"]["userId"],
'apikey':config["TrueCaptcha"]["apiKey"]
}
result = requests.post(url, json.dumps(data))
res=result.json()
try:verifycode = res['result']
except:
if res.get('success') == False:
print(f"{res['error_type']} {res['error_message']}")
if 'Credits' in res['error_message']:
print("TrueCaptcha已达每日请求上限无法再识别验证码。")
return None
else:verifycode = apitruecaptcha(config, content)
elif res.get('message') == 'Internal server error':verifycode = apitruecaptcha(config, content)
else:verifycode = apitruecaptcha(config, content)
return verifycode
def get_tenant_code(school_name: str) -> str:
tenant_list = requests.get(
"https://weiban.mycourse.cn/pharos/login/getTenantListWithLetter.do"
).text
data = json.loads(tenant_list)["data"]
for i in data:
for j in i["list"]:
if j["name"] == school_name:
return j["code"]
def set_accounts():
global DEFAULT_SCHOOL_NAME
with open("config.json", "r+", encoding='utf8') as file:
try:config = json.load(file)
except:
config = {}
config['TrueCaptcha'] = None
config['Accounts'] = []
if config.get("TrueCaptcha") is None:
print('验证码识别使用 TrueCaptcha.org如果你想手动识别验证码请按 Ctrl + C')
try:
config["TrueCaptcha"] = {}
config["TrueCaptcha"]["userId"] = input('请输入 TrueCaptcha.org 的 userId')
config["TrueCaptcha"]["apiKey"] = input('请输入 TrueCaptcha.org 的 apiKey')
if config["TrueCaptcha"]["userId"] == '' or config["TrueCaptcha"]["apiKey"] == '':config["TrueCaptcha"] = None
except KeyboardInterrupt:config["TrueCaptcha"] = None
if config.get("TrueCaptcha") is None:print('\n你选择了手动识别验证码。\n')
print('输入学校名、帐号、密码,结束输入请按 Ctrl + C')
try:
if config["Accounts"]:DEFAULT_SCHOOL_NAME = config["Accounts"][-1]['schoolName']
while True:
print(f'正在录入第 {len(config["Accounts"])+1} 个帐号')
account = {}
# 如果直接按回车则将DEFAULT_SCHOOL_NAME的值赋给schoolName否则将schoolName的值赋给DEFAULT_SCHOOL_NAME
account['schoolName'] = input(f'请输入学校名称(当前默认学校为 {DEFAULT_SCHOOL_NAME}')
if account['schoolName'] == '':account['schoolName'] = DEFAULT_SCHOOL_NAME
else:DEFAULT_SCHOOL_NAME = account['schoolName']
account['id'] = input('请输入学号:')
account['password'] = input('请输入密码:')
account['State'] = 0
if account['id'] == '' or account['password'] == '':
print(f'\n停止输入账号,已保存 {len(config["Accounts"])} 个帐号')
break
config['Accounts'].append(account)
except KeyboardInterrupt:print(f'\n停止输入账号,已保存 {len(config["Accounts"])} 个帐号')
with open('config.json', 'w', encoding='utf8') as file:
file.write(json.dumps(config, indent=4, ensure_ascii=False))
print('配置已保存。\n')
return config
def get_Login_State(config : dict, account : dict) -> dict:
'''
传入参数 config - 配置内容
传入参数 account - 一组账户信息
```json
{
"schoolName": "XX学校",
"id": "20230001",
"password": "12345678",
"State": 0
}
```
以字典形式 返回该账户的登录态
```json
{
"token": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"userId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"tenantCode": "00000001",
"realName": "张三"
}
```
'''
school_name = account['schoolName']
tenant_code = get_tenant_code(school_name=school_name)
user_id = account['id']
user_pwd = account['password']
now = time.time()
# 打开验证码
img_data = requests.get(f"https://weiban.mycourse.cn/pharos/login/randLetterImage.do?time={now}").content
if config['TrueCaptcha'] is None:
print("验证码链接:",end='')
print(f"https://weiban.mycourse.cn/pharos/login/randLetterImage.do?time={now}")
with open("code.jpg", "wb") as file:
file.write(img_data)
file.close()
Image.open("code.jpg").show()
# 获取验证码
verity_code = input("请输入验证码:")
os.remove("code.jpg")
else:
verity_code = apitruecaptcha(config, img_data)
# 调用js方法
payload = {
"userName": user_id,
"password": user_pwd,
"tenantCode": tenant_code,
"timestamp": now,
"verificationCode": verity_code
}
ret = login(payload)
request_data = {"data": ret}
response = requests.post(
"https://weiban.mycourse.cn/pharos/login/login.do", data=request_data
).text
response = json.loads(response)
if response['code'] == '0':
tenantCode = response.get('data').get('tenantCode')
userId = response.get('data').get('userId')
x_token = response.get('data').get('token')
realName = response.get('data').get('realName')
print(f"用户 {user_id} {realName} 登录成功")
return {"token":x_token,"userId":userId,"tenantCode":tenantCode,"realName":realName,"raw_id":user_id}
elif "账号与密码不匹配" in response["msg"] or "账号已被锁定" in response["msg"] or "权限错误" in response["msg"]:
print(f'用户 {user_id} 登录失败,错误码 {response["code"]} 原因为 {response["msg"]}')
return {"is_locked":True,"raw_id":user_id}
else:
print(f'用户 {user_id} 登录失败,错误码 {response["code"]} 原因为 {response["msg"]}')
return get_Login_State(config, account)
def save_Login_State(config):
if config.get('Accounts_login_state') is None or len(config['Accounts_login_state']) == 0:
config['Accounts_login_state'] = []
for account in config.get("Accounts"):
if account['State'] == 1:print(f'用户 {account["id"]} 已经完成,跳过登录')
elif account['State'] == 0:
login_State = get_Login_State(config, account)
if login_State.get("is_locked") is True:account['State'] = -1
else:config['Accounts_login_state'].append(login_State)
elif account['State'] == -1:print(f'用户 {account["id"]} 密码错误,无法登录')
with open('config.json', 'w', encoding='utf8') as file:file.write(json.dumps(config, indent=4, ensure_ascii=False))
print('登录态已保存。\n')
else:print('已存在登录态,跳过登录。\n')

View File

@ -1,4 +0,0 @@
[
{"token":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","userId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","tenantCode":"00000001","userProjectId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"},
{"token":"yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy","userId":"yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy","tenantCode":"00000002","userProjectId":"yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"}
]

58
main.py
View File

@ -2,38 +2,62 @@
# @repo : https://github.com/pooneyy/weiban-tool
import os, sys
import random
import Utils
import json
import asyncio
async def weibanTask(user):
# tenantCode UserId x-token userProjectId
# tenantCode id x-token userProjectId
tenantCode = user.get('tenantCode')
userId = user.get('userId')
x_token = user.get('token')
userProjectId = user.get('userProjectId')
realName = user.get('realName',userId)
realName = user.get('realName')
id = user.get('raw_id')
taskName = '未知的任务名'
main = Utils.main(tenantCode, userId, x_token, userProjectId)
main = Utils.main(tenantCode, userId, x_token, realName)
main.init()
try:
realName = main.getRealName()
taskName = main.getTaskName()
main.get_Project_Info()
taskName = main.taskName
print(f"开始进行 {realName} 的任务:{taskName}")
finishIdList = main.getFinishIdList()
for i in main.getCourse():
await main.start(i)
await asyncio.sleep(20)
main.finish(finishIdList[i])
print(f"{realName} 的任务已完成")
except json.decoder.JSONDecodeError:print(f'{realName} 的账户信息错误或已经过期请重新获取。详见https://github.com/pooneyy/weiban-tool')
# 获取列表
for chooseType in [2,3]:
finishIdList = main.getFinishIdList(chooseType)
index = 1
for i in main.getCourse(chooseType):
print(f"{realName} 开始学习 {main.resourceNames[index]} {index} / {len(finishIdList)}")
await main.start(i)
await asyncio.sleep(random.randint(15,20))
main.finish(i, finishIdList[i])
print(f"{realName} 完成学习 {main.resourceNames[index]}")
index += 1
print(f"{id} {realName} 的任务已完成")
with open("config.json", "r+", encoding='utf8') as file:
config = json.load(file)
for i in config['Accounts']:
if i.get('id') == id:i['State'] = 1
for i in config['Accounts_login_state']:
if i.get('raw_id') == id:config['Accounts_login_state'].remove(i)
# seek(0), truncate()用于覆写文件
file.seek(0)
file.truncate()
json.dump(config, file, ensure_ascii=False, indent=4)
except json.decoder.JSONDecodeError:
print(f'{realName} 的账户登录态已经过期,已删除该登录态。请重新登录。')
with open("config.json", "r+", encoding='utf8') as file:
config = json.load(file)
config['Accounts_login_state'] = []
file.seek(0)
file.truncate()
json.dump(config, file, ensure_ascii=False, indent=4)
except KeyboardInterrupt:print(f'{realName} 的任务被手动终止')
async def main():
usersConfig = {}
usersConfig = []
try:
with open("config.json", "r+", encoding='utf8') as file:
try:usersConfig = json.load(file)
try:usersConfig = json.load(file).get('Accounts_login_state')
except json.decoder.JSONDecodeError:print('配置文件格式错误,请仔细检查 config.json 。详见https://github.com/pooneyy/weiban-tool')
tasks=[]
for user in usersConfig:
@ -43,6 +67,8 @@ async def main():
except FileNotFoundError:print('未找到 config.json详见https://github.com/pooneyy/weiban-tool')
if __name__ =='__main__':
try:asyncio.run(main())
try:
Utils.save_Login_State(Utils.set_accounts())
asyncio.run(main())
except KeyboardInterrupt:print(f'\n任务被手动终止')
os.system("pause")

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
requests~=2.28.2
DateTime~=5.1
Pillow~=9.5.0
pycryptodomex