├── .gitignore ├── models.pck ├── crypto.py ├── captcha.py └── flash.py /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist 3 | /flash.spec -------------------------------------------------------------------------------- /models.pck: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mufanc/SafetyFlasher/HEAD/models.pck -------------------------------------------------------------------------------- /crypto.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES 2 | from base64 import b64encode 3 | 4 | 5 | def pkcs7_fix(plain): 6 | padding_size = (AES.block_size - len(plain) % AES.block_size) 7 | return plain + padding_size * chr(padding_size) 8 | 9 | 10 | def encrypt(passwd, data): 11 | passwd += '\x00' * (16 - len(passwd)) 12 | cipher = AES.new(passwd.encode(), AES.MODE_ECB) 13 | encrypted = cipher.encrypt(pkcs7_fix(data).encode()) 14 | return b64encode(encrypted).decode().replace('+', '-').replace('/', '_') 15 | -------------------------------------------------------------------------------- /captcha.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import pickle 3 | import numpy as np 4 | from queue import Queue 5 | 6 | inf = 2147483647 7 | models = pickle.load(open('models.pck', 'rb')) 8 | 9 | 10 | def bfs(img, flag, i, j): 11 | ways = ((-1, 0), (1, 0), (0, -1), (0, 1)) 12 | task = Queue() 13 | flag[i, j] = 1 14 | task.put((i, j)) 15 | color: np.ndarray = img[i, j] 16 | min_i, max_i, min_j, max_j = inf, 0, inf, 0 17 | size = 0 18 | while task.qsize(): 19 | ti, tj = task.get() 20 | size += 1 21 | min_i, max_i, min_j, max_j = min(min_i, ti), max(max_i, ti), min(min_j, tj), max(max_j, tj) 22 | for w in ways: 23 | ni, nj = ti+w[0], tj+w[1] 24 | if ni >= len(img) or ni < 0: 25 | continue 26 | if nj >= len(img[0]) or nj < 0: 27 | continue 28 | if not (all(img[ni, nj] == (255, 255, 255)) or flag[ni, nj]) and np.all(img[ni, nj] == color): 29 | flag[ni, nj] = 1 30 | task.put((ni, nj)) 31 | if size < 20: 32 | return None 33 | return img[min_i:max_i+1, min_j:max_j+1] 34 | 35 | 36 | def binary(img): 37 | count = {} 38 | for line in img: 39 | for px in line: 40 | c = [int(n) for n in px] 41 | key = c[0] << 16 | c[1] << 8 | c[2] 42 | if key == 16777215: 43 | continue 44 | if key not in count: 45 | count[key] = 0 46 | count[key] += 1 47 | color = '' 48 | count[''] = 0 49 | for n in count: 50 | if count[n] > count[color]: 51 | color = n 52 | color = np.array((color >> 16, color >> 8 & 0xff, color & 0xff)) 53 | out = np.zeros((len(img), len(img[0]), 3), dtype=np.uint8) 54 | out[np.where(color != img)] = 255 55 | return out 56 | 57 | 58 | def similarity(im1, im2): # img, model 59 | im1 = im1[:, :, 0] 60 | im2 = cv2.resize(im2, (len(im1[0]), len(im1)), interpolation=cv2.INTER_LINEAR)[:, :, 0] 61 | sim = -int(np.sum(np.abs(im1-im2))) 62 | return sim 63 | 64 | 65 | def detect(data): 66 | img = cv2.imdecode(np.frombuffer(data, np.uint8), cv2.IMREAD_COLOR) # 解码图片 67 | img = img[:, :-20] 68 | flag = np.zeros((len(img), len(img[0])), dtype=np.uint8) 69 | dec = [] 70 | for j in range(len(img[0])): 71 | for i in range(len(img)): 72 | if not (all(img[i, j] == (255, 255, 255)) or flag[i, j]): 73 | sub = bfs(img, flag, i, j) 74 | if sub is not None: 75 | dec.append(binary(sub)) 76 | exp = '' 77 | for img in dec: 78 | sims = [(similarity(img, m), i) for i, m in enumerate(models)] 79 | exp += '1234567890-+'[sorted(sims)[-1][1]] 80 | try: 81 | return str(eval(exp)) 82 | except Exception: 83 | return '' 84 | -------------------------------------------------------------------------------- /flash.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | import tkinter as tk 4 | from io import BytesIO 5 | from random import randint 6 | from time import time, sleep 7 | 8 | import requests 9 | from PIL import Image, ImageTk 10 | from loguru import logger 11 | 12 | from captcha import detect 13 | from crypto import encrypt 14 | 15 | logger.remove() 16 | logger.add(sys.stdout, format='[{time:HH:MM:SS}] {message}') 17 | 18 | 19 | def timestamp(): 20 | return f'{int(time())}' 21 | 22 | 23 | class WeibanClient(object): 24 | def __init__(self): 25 | self.userinfo = {} 26 | self.session = requests.session() 27 | self.current_request = None 28 | self.session.headers.update({ 29 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' 30 | 'Chrome/93.0.4544.0 Safari/537.36' 31 | }) 32 | self.last_choice = {} 33 | 34 | def login_with_qrcode(self): 35 | logger.debug('正在获取二维码...') 36 | qrcode = self.session.get('https://weiban.mycourse.cn/pharos/login/genBarCodeImageAndCacheUuid.do', params={ 37 | 'timestamp': timestamp() 38 | }).json()['data'] 39 | image = Image.open(BytesIO(self.session.get(qrcode['imagePath']).content)) 40 | logger.debug('请扫码完成登录') 41 | root = tk.Tk() 42 | root.title('扫码登录后关闭此窗口') 43 | image = ImageTk.PhotoImage(image) 44 | tk.Label(root, image=image).pack() 45 | root.lift() 46 | root.attributes('-topmost', True) 47 | root.after_idle(root.attributes, '-topmost', False) 48 | root.mainloop() 49 | sleep(3) 50 | result = self.post('https://weiban.mycourse.cn/pharos/login/barCodeWebAutoLogin.do', { 51 | 'barCodeCacheUserId': qrcode['barCodeCacheUserId'] 52 | })['data'] 53 | self.userinfo = {key: result[key] for key in result if key in ('token', 'userId', 'tenantCode')} 54 | logger.warning('登录成功!') 55 | 56 | def login_with_password(self, tenant, username, password): 57 | tenant_list = self.session.get('https://weiban.mycourse.cn/pharos/login/getTenantList.do', params={ 58 | 'timestamp': timestamp() 59 | }).json()['data'] 60 | tenant_code = '' 61 | for item in tenant_list: 62 | if item['name'] == tenant: 63 | tenant_code = item['code'] 64 | break 65 | else: 66 | logger.error('暂无此学校/社区的信息!') 67 | exit() 68 | for trial in range(10): 69 | captcha = self.session.get('https://weiban.mycourse.cn/pharos/login/randImage.do', params={ 70 | 'time': timestamp() 71 | }) 72 | verify_code = detect(captcha.content) 73 | data = encrypt('xie2gg', json.dumps({ 74 | 'keyNumber': username, 75 | 'password': password, 76 | 'tenantCode': tenant_code, 77 | 'time': timestamp(), 78 | 'verifyCode': verify_code 79 | })) 80 | result = self.post('https://weiban.mycourse.cn/pharos/login/login.do', { 81 | 'data': data 82 | }) 83 | if result['code'] == '0': 84 | result = result['data'] 85 | self.userinfo = {key: result[key] for key in result if key in ('token', 'userId', 'tenantCode')} 86 | logger.warning('登录成功!') 87 | break 88 | elif result['code'] == '-1' and result['detailCode'] == '67': 89 | logger.warning('验证码错误,即将重新识别') 90 | else: 91 | logger.error(f'未知错误:{result}') 92 | exit() 93 | else: 94 | logger.error('尝试次数过多') 95 | exit() 96 | 97 | def login_manually(self): 98 | logger.warning('在电脑浏览器访问 https://weiban.mycourse.cn,然后选择右侧「账号登录」进行登录') 99 | logger.debug('登录后将下列内容复制到浏览器地址栏并按回车:') 100 | print('javascript:(function(){data=JSON.parse(localStorage.user);prompt(\'\',JSON.stringify({token:data[' 101 | '\'token\'],userId:data[\'userId\'], tenantCode:data[\'tenantCode\']}));})();') 102 | logger.warning('(注意某些浏览器会把开头的“javascript:”吞掉,如果粘贴后发现没有请自行补上)') 103 | self.userinfo = json.loads(input('把弹窗显示的内容粘贴到这里:')) 104 | 105 | def post(self, url, data={}): 106 | return self.session.post(url, params={'timestamp': timestamp()}, data={ 107 | **self.userinfo, 108 | **data 109 | }).json() 110 | 111 | def list_study_task(self): 112 | return self.post('https://weiban.mycourse.cn/pharos/index/listStudyTask.do')['data'] 113 | 114 | def list_category(self, user_project_id): 115 | categories = [] 116 | for T in (3, 1, 2): 117 | result = self.post('https://weiban.mycourse.cn/pharos/usercourse/listCategory.do', data={ 118 | 'userProjectId': user_project_id, 119 | 'chooseType': T 120 | })['data'] 121 | categories.append(result) 122 | return categories 123 | 124 | def list_courses(self, user_project_id, category_code, choose_type): 125 | return self.post('https://weiban.mycourse.cn/pharos/usercourse/listCourse.do', { 126 | 'userProjectId': user_project_id, 127 | 'categoryCode': category_code, 128 | 'chooseType': choose_type, 129 | 'name': '' 130 | })['data'] 131 | 132 | def start_study(self, user_project_id, resource_id): 133 | result = self.post('https://weiban.mycourse.cn/pharos/usercourse/study.do', { 134 | 'userProjectId': user_project_id, 135 | 'courseId': resource_id 136 | }) 137 | logger.info(f'study.do -> {result}') 138 | course_url = self.post('https://weiban.mycourse.cn/pharos/usercourse/getCourseUrl.do', data={ 139 | 'userProjectId': user_project_id, 140 | 'courseId': resource_id 141 | })['data'] 142 | logger.info(f'getCourseUrl.do -> {result}') 143 | logger.info(f'getting... -> {self.session.get(course_url).status_code}') 144 | 145 | def send_finish(self, user_course_id): 146 | return self.session.post( 147 | 'https://weiban.mycourse.cn/pharos/usercourse/finish.do', { 148 | '_': timestamp(), 149 | 'userCourseId': user_course_id, 150 | 'tenantCode': self.userinfo['tenantCode'] 151 | } 152 | ).content.decode() 153 | 154 | def show_progress(self, user_project_id): 155 | return self.post('https://weiban.mycourse.cn/pharos/project/showProgress.do', { 156 | 'userProjectId': user_project_id 157 | })['data'] 158 | 159 | def optional_finished(self, user_project_id): 160 | progress = self.show_progress(user_project_id) 161 | return progress['optionalFinishedNum'] >= progress['optionalNum'] 162 | 163 | def flash(self): 164 | tasks = self.list_study_task() 165 | logger.debug('发现课程列表:') 166 | for i, task in enumerate(tasks): 167 | logger.info(f'{i+1: >2d}: {task["projectName"]}') 168 | if len(tasks) > 1: 169 | if 'index' in self.last_choice: 170 | index = self.last_choice['index'] 171 | else: 172 | index = int(input('输入要刷的课程序号:')) - 1 173 | self.last_choice['index'] = index 174 | else: 175 | index = 0 176 | task = tasks[index] 177 | logger.debug(f'已选择:{task["projectName"]} [id:{task["userProjectId"]}]') 178 | if '-y' not in self.last_choice: 179 | logger.warning('确认开始刷课吗?'), input() 180 | self.last_choice['-y'] = True 181 | categories = self.list_category(task['userProjectId']) 182 | for T, name in enumerate(('必修课程', '匹配课程', '自选课程')): 183 | logger.debug(f'获取到任务列表 [{name}]:') 184 | categories_to_flash = [] 185 | for i, cat in enumerate(categories[T]): 186 | logger.info(f'{i+1: >2d}: {cat["categoryName"]} | 已完成:{cat["finishedNum"]}/{cat["totalNum"]}') 187 | if cat["finishedNum"] < cat["totalNum"]: 188 | categories_to_flash.append(cat) 189 | if not categories_to_flash: 190 | logger.warning('该课程已完成,跳过') 191 | continue 192 | if name == '自选课程' and self.optional_finished(task['userProjectId']): 193 | logger.warning('自选课程已完成!') 194 | continue 195 | for cat in categories_to_flash: 196 | logger.warning(f'正在刷课:{name} -> {cat["categoryName"]}') 197 | courses = self.list_courses(task['userProjectId'], cat['categoryCode'], (3, 1, 2)[T]) 198 | for i, course in enumerate(courses): 199 | logger.debug(f'{i+1: >2d}: {course["resourceName"]}') 200 | if course['finished'] == 1: 201 | logger.debug('该项已完成,跳过') 202 | continue 203 | self.start_study(task['userProjectId'], course['resourceId']) 204 | wait = randint(10, 20) 205 | logger.warning(f'随机等待 {wait} 秒...') 206 | sleep(wait) 207 | logger.debug(f'Finishing... -> {self.send_finish(course["userCourseId"])}') 208 | logger.debug('3 秒后继续下一课程') 209 | sleep(3) 210 | progress = self.show_progress(task['userProjectId']) 211 | logger.debug('必修课程:[{requiredFinishedNum}/{requiredNum}] | 匹配课程:[{pushFinishedNum}/{pushNum}] | 自选课程:[{' 212 | 'optionalFinishedNum}/{optionalNum}]'.format(**progress)) 213 | logger.warning('全部课程刷课完毕!') 214 | input() 215 | 216 | 217 | def main(): 218 | logger.warning('登录后请先完成初始测试,否则可能产生意料之外的后果(如果没有请自动忽略)') 219 | logger.info('按回车键继续...'), input() 220 | 221 | logger.debug('请选择登录方式:') 222 | for i, method in enumerate(('扫码登录(默认)', '账号密码登录', '手动登录')): 223 | logger.info(f'{i+1}: {method}') 224 | client = WeibanClient() 225 | method = int(input() or '1') 226 | if method == 1: 227 | client.login_with_qrcode() 228 | elif method == 2: 229 | tenant = input('请输入学校/社区名称:') 230 | username = input('用户名:') 231 | password = input('密码:') 232 | client.login_with_password(tenant, username, password) 233 | elif method == 3: 234 | client.login_manually() 235 | else: 236 | logger.error('请输入正确的序号!') 237 | exit() 238 | for trial in range(10): 239 | try: 240 | client.flash() 241 | break 242 | except Exception as err: 243 | logger.error(repr(err)) 244 | logger.warning('发生错误,5 秒后重试') 245 | sleep(5) 246 | else: 247 | logger.error('重试次数过多!') 248 | 249 | 250 | if __name__ == '__main__': 251 | try: 252 | main() 253 | except Exception as error: 254 | logger.error(repr(error)) 255 | logger.warning('Exiting...') 256 | except KeyboardInterrupt: 257 | logger.warning('Exiting...') 258 | --------------------------------------------------------------------------------