├── README.md ├── config └── config.py ├── control.py ├── index.py ├── library ├── consts.py ├── manager.py └── seatsys.py └── utils ├── loginfo.py ├── myexcept.py ├── push.py ├── synfile.py ├── utils.py └── weather.py /README.md: -------------------------------------------------------------------------------- 1 | 2 | # 预约系统:我去图书馆 3 | 1. 动预约请将本项目部署至云函数 4 | 2. 请自行下载 App 抓包获取 用户 token 5 | 3. 其他问题请自行解决,本项目不再维护 6 | 7 | 8 | ## 模块说明 9 | - 入口函数:`index.handler()`,传入使用者列表 10 | - 初始化函数:`index.initializer()`,提前准备预约cookies等数据 11 | - 函数 `reserveFun()`:管理单线程预约功能 12 | - 类 `SeatSystem()`:座位预约类 13 | - 类 `ReservManager()`:管理控制所有预约进程 14 | 15 | 16 | ## 维护日志 17 | - 9.22.10 修复手动续约时的强制退座 18 | - 9.19.11 加入预约试探,修复预约不退送问题 (未解决) 19 | - 9.17.12 完善邮件推送功能 20 | - 9.17.12 完善日志控制功能 21 | - 9.14.23 修复 续约座位错误 22 | - 9.07.23 修复 座位切换bug 23 | - 9.05.23 修复 本地文件读取错误 24 | - 9.05.14 修复 云端及本地 cookies 同步问题 25 | 26 | 27 | ## 问题记录 28 | - 主子线程通信时,queue 队列,子线程未完成,则队列为空 29 | - set.difference 比较差异需要正反比较 30 | - 紧接着定义在类下的变量是局部变量 31 | - Thread.join 可以阻塞调用它的线程 -------------------------------------------------------------------------------- /config/config.py: -------------------------------------------------------------------------------- 1 | 2 | #!程序隐私信息以及所用用户的配置信息 ################# 3 | #!############################################## 4 | user = 'xxxx@xx.com' # !发送方的邮箱地址 5 | passwd = 'xxxxxxxxxxxx' # !发送方邮箱授权码 6 | servchan = "xxxxxxx" # !微信:方糖推送 7 | #!############################################## 8 | webdav_usr = "xxxx@xx.com" # !坚果云账号 9 | webdav_pwd = "xxxxxxxxxxx" 10 | #!############################################## 11 | 12 | 13 | USER_NAME1 = { 14 | # token 是最重要的,用于获取图书馆座位信息 15 | "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9", 16 | "email": "xxxx@xx.com", 17 | "seat_prefer": 1, 18 | "name": "USER_NAME1" 19 | } 20 | USER_NAME2 = { 21 | # token 是最重要的,用于获取图书馆座位信息 22 | "token": "", 23 | "email": "xxxx@xx.com", 24 | "seat_prefer": 1, 25 | "name": "USER_NAME2" 26 | } 27 | -------------------------------------------------------------------------------- /control.py: -------------------------------------------------------------------------------- 1 | from library.seatsys import SeatSystem 2 | from utils.push import weatherinfo 3 | from config.config import * 4 | # 此文件是 manager 模块的依赖文件 5 | # 为方便能修改预约功能,故提升至此 6 | 7 | 8 | def reserveFun(user)->tuple[dict,dict,str]: 9 | """ 预约功能控制函数 """ 10 | 11 | seat = SeatSystem(user) 12 | 13 | # seat.reserveTimer([7, 0, 0]) 14 | # seat.reserveRenew([8,29, 0]) 15 | # seat.reserveCancel([8,58,0]) 16 | # seat.closeCancel([22, 0, 0]) 17 | 18 | seat.reserveSeat() # 可试探预约处理 19 | 20 | seat.updateReserveInfo() 21 | 22 | info_jar = seat.logs.getInfoJar() # 程序运行信息包 23 | 24 | if user['name'] == "USER_NAME1": 25 | info_jar['ispush' ] = False 26 | info_jar['wxpush' ] = False 27 | info_jar['content'] = f"{weatherinfo()}\n{info_jar['content']}" 28 | 29 | return info_jar 30 | 31 | -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | from library.manager import ReserveManager 2 | from utils.synfile import WebDavCookies 3 | from library.consts import * 4 | from config.config import * 5 | 6 | 7 | def initializer(event=None): 8 | """ 提前初始化数据 """ 9 | # WebDavCookies.down_cookies() 10 | pass 11 | 12 | 13 | def handler(event=None, text=None): 14 | """ 程序总控入口 """ 15 | 16 | # 将需要预约的用户加入列表 17 | USRLST = [USER_NAME1, USER_NAME2] 18 | 19 | res = ReserveManager(USRLST) 20 | res.printLogs() 21 | res.pushLogs("wxpush", [7,0,0], title=" 🔵 程序预约日志-服务器C(本地)") 22 | 23 | 24 | if __name__ == '__main__': 25 | 26 | handler() 27 | # handler() -------------------------------------------------------------------------------- /library/consts.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | # 此文件用于存放程序的常量 3 | 4 | 5 | # cookies 文件路径,磁盘根目录/tmp 6 | LOCAL_PATH = "/tmp/cookies.json" 7 | LOCAL_PATH2 = "/tmp/cookies2.json" 8 | JIAN_PATH = "/我去图书馆/cookies.json" 9 | JIAN_PATH2 = "/我去图书馆/cookies2.json" 10 | 11 | PATH_PAIR1 = [LOCAL_PATH , JIAN_PATH ] 12 | PATH_PAIR2 = [LOCAL_PATH2, JIAN_PATH2] 13 | 14 | # 场馆的座位状态 seat_status 15 | # 个人的座位状态 status 16 | # 无人(白色) status=1 17 | # 已选(绿色) status=2 18 | # 有人(红色) status=3 19 | # 暂离(灰色) status=4 20 | 21 | 22 | class Operate(Enum): 23 | 24 | athHead = { 25 | "User-Agent": 26 | "Mozilla/5.0 (Linux; Android 11; Redmi K20 Pro Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.99 XWEB/3171 MMWEBSDK/20220303 Mobile Safari/537.36 MMWEBID/7166 MicroMessenger/8.0.21.2103(0x28001541) Process/appbrand0 WeChat/arm64 Weixin GPVersion/1 NetType/4G Language/zh_CN ABI/arm64 MiniProgramEnv/android", 27 | "X-Requested-With": "com.tencent.mm", 28 | "Host": "wechat.v2.traceint.com", 29 | 30 | } 31 | pstHead = { 32 | "X-Requested-With": "com.tencent.mm", 33 | "User-Agent": 34 | "Mozilla/5.0 (Linux; Android 11; Redmi K20 Pro Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.99 XWEB/3171 MMWEBSDK/20220303 Mobile Safari/537.36 MMWEBID/7166 MicroMessenger/8.0.21.2103(0x28001541) Process/appbrand0 WeChat/arm64 Weixin GPVersion/1 NetType/4G Language/zh_CN ABI/arm64 MiniProgramEnv/android", 35 | "Content-Type": "application/json", 36 | "Referer": "http://web.traceint.com/web/index.html", 37 | "Origin": "http://web.traceint.com", 38 | "Sec-Fetch-Dest": "empty", 39 | "Sec-Fetch-Mode": "cors", 40 | "Sec-Fetch-Site": "same-site", 41 | "Connection": "keep-alive", 42 | "Host": "wechat.v2.traceint.com", 43 | } 44 | 45 | index = { 46 | "operationName": "index", 47 | "query": 48 | "query index($pos: String!, $param: [hash]) {\n userAuth {\n oftenseat {\n list {\n id\n info\n lib_id\n seat_key\n status\n }\n }\n message {\n new(from: \"system\") {\n has\n from_user\n title\n num\n }\n indexMsg {\n message_id\n title\n content\n isread\n isused\n from_user\n create_time\n }\n }\n reserve {\n reserve {\n token\n status\n user_id\n user_nick\n sch_name\n lib_id\n lib_name\n lib_floor\n seat_key\n seat_name\n date\n exp_date\n exp_date_str\n validate_date\n hold_date\n diff\n diff_str\n mark_source\n isRecordUser\n isChooseSeat\n isRecord\n mistakeNum\n openTime\n threshold\n daynum\n mistakeNum\n closeTime\n timerange\n forbidQrValid\n renewTimeNext\n forbidRenewTime\n forbidWechatCancle\n }\n getSToken\n }\n currentUser {\n user_id\n user_nick\n user_mobile\n user_sex\n user_sch_id\n user_sch\n user_last_login\n user_avatar(size: MIDDLE)\n user_adate\n user_student_no\n user_student_name\n area_name\n user_deny {\n deny_deadline\n }\n sch {\n sch_id\n sch_name\n activityUrl\n isShowCommon\n isBusy\n }\n }\n }\n ad(pos: $pos, param: $param) {\n name\n pic\n url\n }\n}", 49 | "variables": { 50 | "pos": "App-首页" 51 | } 52 | } 53 | 54 | getUserCancleConfig = { 55 | "operationName": "getUserCancleConfig", 56 | "query": "query getUserCancleConfig {\n userAuth {\n user {\n holdValidate: getSchConfig(fields: \"hold_validate\", extra: true)\n }\n }\n}", 57 | "variables": {} 58 | } 59 | 60 | reserveSeat = { 61 | "operationName": "reserveSeat", 62 | "query": 63 | "mutation reserveSeat($libId: Int!, $seatKey: String!, $captchaCode: String, $captcha: String!) {\n userAuth {\n reserve {\n reserveSeat(\n libId: $libId\n seatKey: $seatKey\n captchaCode: $captchaCode\n captcha: $captcha\n )\n }\n }\n}", 64 | "variables": { 65 | "seatKey": "", 66 | "libId": 122804, 67 | "captchaCode": "", 68 | "captcha": "" 69 | } 70 | } 71 | reserveCancle = { 72 | "operationName": "reserveCancle", 73 | "query": 74 | "mutation reserveCancle($sToken: String!) {\n userAuth {\n reserve {\n reserveCancle(sToken: $sToken) {\n timerange\n img\n hours\n mins\n per\n }\n }\n }\n}", 75 | "variables": { 76 | "sToken": "" 77 | } 78 | } 79 | getList = { 80 | "operationName": 81 | "getList", 82 | "query": 83 | "query getList {\n userAuth {\n credit {\n tasks {\n id\n task_id\n task_name\n task_info\n task_url\n credit_num\n contents\n conditions\n task_type\n status\n }\n staticTasks {\n id\n name\n task_type_name\n credit_num\n contents\n button\n }\n }\n }\n}" 84 | } 85 | doneTask = { 86 | "operationName": "done", 87 | "query": 88 | "mutation done($user_task_id: Int!) {\n userAuth {\n credit {\n done(user_task_id: $user_task_id)\n }\n }\n}", 89 | "variables": { 90 | "user_task_id": 0 # 需要补全参数 user_task_id 91 | } 92 | } 93 | user_credit = { 94 | "operationName": 95 | "user_credit", 96 | "query": 97 | "query user_credit {\n userAuth {\n currentUser {\n user_credit\n }\n }\n}" 98 | } 99 | libLayout = { 100 | "operationName": "libLayout", 101 | "query": 102 | "query libLayout($libId: Int, $libType: Int) {\n userAuth {\n reserve {\n libs(libType: $libType, libId: $libId) {\n lib_id\n is_open\n lib_floor\n lib_name\n lib_type\n lib_layout {\n seats_total\n seats_booking\n seats_used\n max_x\n max_y\n seats {\n x\n y\n key\n type\n name\n seat_status\n status\n }\n }\n }\n }\n }\n}", 103 | "variables": { 104 | "libId": 122804 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /library/manager.py: -------------------------------------------------------------------------------- 1 | import time 2 | from queue import Queue 3 | from threading import Thread 4 | from typing import List 5 | 6 | from control import reserveFun 7 | from utils.myexcept import TokenError 8 | from utils.push import privatePush, sendEmail 9 | from utils.utils import openJson, writeJson, timeRange 10 | from library.consts import PATH_PAIR1 11 | 12 | 13 | def exceptControl(user)->dict: 14 | """ 流程控制、异常处理 15 | 在函数 funControl(user) 的基础上增加异常处理 16 | """ 17 | 18 | info_jar = {} # msg_jar 程序运行信息包 19 | for i in range(5): 20 | 21 | try: # 如果正常的话就正常运行 22 | info_jar = reserveFun(user) 23 | return info_jar 24 | 25 | except TokenError as e: # Token 无效,终止程序 26 | info_jar['except'] += f"\n用户令牌错误,程序退出:{e}" 27 | return info_jar 28 | 29 | except Exception as e: # 网络不稳定时,需重新连接 30 | if i >= 4: 31 | info_jar['except'] += f"\n多次连接失败,程序退出:{e}" 32 | return info_jar 33 | 34 | # def exceptControl(user)->dict: 35 | # info_jar = {} 36 | # info_jar = reserveFun(user) 37 | # return info_jar 38 | 39 | class ResrveThread(Thread): 40 | """ 预约定制子线程""" 41 | 42 | def __init__(self, que:Queue, usr): 43 | super().__init__() 44 | self.que = que 45 | self.usr = usr 46 | 47 | def run(self): 48 | jars = exceptControl(self.usr) 49 | self.que.put(jars) 50 | 51 | 52 | class ReserveManager(): 53 | """ 预约管理类,单例 """ 54 | 55 | QUE = Queue() 56 | 57 | 58 | def __init__(self, uesrlist): 59 | self.userlist = uesrlist 60 | self.cookdicts = {} 61 | self.threads: List[Thread] = [] 62 | self.info_list: List[dict] = [] #所有用户的信息 63 | self.logs = "" 64 | 65 | self.__start() 66 | 67 | 68 | def __start(self): 69 | """ 开始批量处理程序""" 70 | 71 | for usr in self.userlist: 72 | self.threads.append(ResrveThread(self.QUE, usr)) 73 | 74 | # 循环开启所有子线程 75 | for t in self.threads: t.start() 76 | for t in self.threads: t.join() 77 | 78 | que_cnt = 0 79 | while True: 80 | if not self.QUE.empty(): 81 | 82 | info_jar = self.QUE.get() 83 | self.info_list.append(info_jar) 84 | 85 | que_cnt += 1 86 | 87 | if que_cnt==len(self.userlist): break 88 | 89 | 90 | def __del__(self): 91 | """ 程序结束时推送邮件信息 """ 92 | 93 | self.pusnInfo() 94 | 95 | 96 | def __neatenLogs(self): 97 | 98 | for info_jar in self.info_list: 99 | name = info_jar.get('name' ) 100 | seat = info_jar.get('seat_name') 101 | lib = info_jar.get('lib_name' ) 102 | statu = info_jar.get('status' ) 103 | logs = info_jar.get('alllogs' ) 104 | excep = info_jar.get('except' ) 105 | title = f"🧊 用户:{name}({lib}{seat}号,{statu})" 106 | if excep==None: 107 | self.logs += f"{title}\n{logs}\n\n" 108 | else: 109 | self.logs += f"{title}\n{logs}\n{excep}\n\n" 110 | 111 | 112 | def printLogs(self): 113 | """ 输出程序运行日志至控制台 """ 114 | 115 | if self.logs=="": 116 | self.__neatenLogs() 117 | 118 | print(self.logs) 119 | 120 | 121 | def pushLogs(self, receive, ontime=[7,0,0], title=None): 122 | """ 发送程序运行日志至邮箱 123 | @receive: email.com 124 | """ 125 | if self.logs=="": self.__neatenLogs() 126 | 127 | try: 128 | if ontime != None: 129 | isend = timeRange(ontime, 5) 130 | if not isend: return 131 | 132 | if title == None: 133 | title = " 🔵 程序预约日志" 134 | 135 | if receive == "wxpush": 136 | privatePush(receive, title, self.logs) 137 | else: 138 | sendEmail(receive, title, self.logs) 139 | 140 | except: pass 141 | 142 | 143 | def pusnInfo(self): 144 | """ 根据配置推送预约信息至各手机""" 145 | for jar in self.info_list: 146 | # 若推送标志位为0、邮件主题为空则不推送 147 | if not jar.get('ispush' ): break 148 | if not jar.get('subject'): break 149 | 150 | msg = jar['email'], jar['subject'], jar['content'] 151 | 152 | if jar.get('wxpush'): 153 | privatePush(*msg) 154 | else: 155 | sendEmail(*msg) 156 | 157 | 158 | def postProcess(self, path_pair=PATH_PAIR1, cookdicts:dict=None): 159 | """ 后处理,是否更新云盘 cookies 文件 """ 160 | for info_jar in self.info_list: 161 | cook_jar = info_jar['cook_jar'] 162 | if cook_jar: 163 | self.cookdicts |= cook_jar 164 | 165 | # 云端及本地是否过期,若过期则写入服务器最新 cookies 166 | if cookdicts==None: 167 | cookdicts = self.cookdicts 168 | try: 169 | latestlst = list(cookdicts.keys()) 170 | latestset = set(latestlst) 171 | 172 | localdict = openJson(path_pair[0]) 173 | localist = list(localdict.keys()) 174 | localset = set(localist) 175 | 176 | # set 正反比较 177 | dff1 = latestset.difference(set(localset)) 178 | dff2 = localset.difference(set(latestset)) 179 | diff = len(dff1) + len(dff2) 180 | localexp = localdict[localist[0]]['EXPIRE'] 181 | isexpire = localexp < int(time.time()) 182 | except: 183 | isexpire = True 184 | diff = True 185 | 186 | # 如果数据变化、数据过期则上传新文件 187 | if isexpire or diff: 188 | w = Thread(target=writeJson, args=(path_pair, cookdicts)) 189 | w.start() 190 | w.join() 191 | 192 | -------------------------------------------------------------------------------- /library/seatsys.py: -------------------------------------------------------------------------------- 1 | from utils.utils import TimerThread, openJson, timeRange, isOverTime 2 | from library.consts import Operate, LOCAL_PATH 3 | from utils.myexcept import TokenError 4 | from utils.loginfo import LogInfo 5 | import requests 6 | import json 7 | import time 8 | import re 9 | requests.packages.urllib3.disable_warnings() 10 | 11 | 12 | class SeatSystem(): 13 | """ 图书馆座位预约系统 """ 14 | 15 | def __init__(self, USER): 16 | self.isresrved = False # 是否预约座位 17 | self.often_seats = [] # 常用座位列表 18 | self.currentuser = {} # 当前用户信息 19 | self.reserveinfo = {} # 座位预约信息 20 | self.stoken = None # 自己退座令牌 21 | self.lib_id = None # 阅览室的 ID 22 | self.lib_name = None # 阅览室的名称 23 | self.seat_key = None # 座位的坐标 24 | self.seat_name = None # 座位的号码 25 | self.status = 0 # 自己座位状态 26 | self.diff_str = 0 # 本在馆学习时长 27 | self.exp_date = 0 # 签到的过期时间 28 | self.cookdict = {} # 获取的cookies 29 | self.cook_jar = {} # 封装cookies包 30 | self.loacal_jar = {} # 本地cookies包 31 | 32 | # 初始化用户的数据信息 33 | self.Name = USER['name'] 34 | self.Email = USER['email'] 35 | self.Token = USER['token'] 36 | self.SeatID = USER['seat_prefer'] 37 | 38 | # 记录运行日志,可发送邮件 39 | self.logs = LogInfo(self.Email, self.Name) 40 | self.logs.putInfo("创建对象,开始运行") 41 | self.cookdict = self.loadCookies() 42 | self.updateReserveInfo() 43 | 44 | 45 | def __del__(self): 46 | """ 销毁时后处理函数 """ 47 | self.signTask() 48 | self.logs.putInfo("执行完毕,结束运行") 49 | 50 | 51 | def loadCookies(self): 52 | """ 从文件中读取并返回 Cookies """ 53 | 54 | try: 55 | self.loacal_jar = openJson(LOCAL_PATH) 56 | data = self.loacal_jar[self.Name] 57 | 58 | # 如果读取的 cookies 未过期,则赋给成员变量 59 | if int(time.time()) < data['EXPIRE' ]: 60 | self.cookdict = data['COOKIES'] 61 | self.logs.putInfo("已获取本地获取数据") 62 | self.cook_jar = { 63 | self.Name: self.loacal_jar[self.Name] 64 | } 65 | else: raise Exception("本地 cookies 过期") 66 | 67 | except Exception as e: 68 | # 如果 cookies 文件不存在,则写入 69 | # self.logs.putLog(f"问题:{e}") 70 | self.logs.putInfo("正在从服务器获取数据...") 71 | self.cookdict = self.gainCookie() 72 | 73 | return self.cookdict 74 | 75 | 76 | def gainCookie(self): 77 | """ 获取 Cookies 并写入文件,有效期 1h """ 78 | paraurl = "https://wechat.v2.traceint.com/index.php/reserve/index.html?f=wechat" 79 | authUrl = f"http://wechat.v2.traceint.com/index.php/urlNew/flagSeat?token={self.Token}" 80 | athHead = Operate.athHead.value 81 | 82 | session = requests.session() 83 | session.keep_alive = False 84 | 85 | session.post(paraurl, headers=athHead ,verify=False) 86 | cookdict1 = requests.utils.dict_from_cookiejar(session.cookies) 87 | 88 | session.post(authUrl, headers=athHead ,verify=False) 89 | cookdict2 = requests.utils.dict_from_cookiejar(session.cookies) 90 | 91 | self.cookdict = cookdict1 | cookdict2 92 | 93 | # 检查是否获取到授权码 Authorization,若没有则需要终止程序 94 | if 'Authorization' not in self.cookdict: 95 | info = "💥 致命错误:图书馆预约失败,请配置有效的 Token" 96 | self.logs.setSubject(info) 97 | raise TokenError(info) 98 | else: self.logs.putInfo("服务器数据获取成功") 99 | 100 | # 将有效时间,以及 获取的 cookies、授权码 存入文件 101 | self.cook_jar = { 102 | self.Name: { 103 | '获取时间': time.strftime("%H:%M:%S", time.localtime()), 104 | 'EXPIRE': int(time.time()) + 3600, 105 | "COOKIES": self.cookdict 106 | } 107 | } 108 | return self.cookdict 109 | 110 | 111 | def updateServerTime(self): 112 | """ 更新请求 SERVERID 的时间 """ 113 | 114 | timestr = f"|{int(time.time())}|" 115 | try: 116 | self.cookdict['SERVERID'] = re.sub("\\|.*?\\|", timestr, self.cookdict['SERVERID']) 117 | except Exception as e: 118 | self.logs.putInfo(f"SERVERID 更新错误:{e}") 119 | self.gainCookie() 120 | 121 | 122 | def postOperation(self, operate: Operate): 123 | """ 操作请求模板 """ 124 | url = "http://wechat.v2.traceint.com/index.php/graphql/" 125 | 126 | try: self.cookdict['Authorization'] 127 | except: self.gainCookie() 128 | 129 | self.updateServerTime() # 更新 SERVERID 时间 130 | head = Operate.pstHead.value 131 | resp = requests.post(url, json=operate.value, headers=head, cookies=self.cookdict, verify=False) 132 | resp = json.loads(resp.text) 133 | 134 | return resp 135 | 136 | 137 | def signTask(self): 138 | """ 每日签到获取积分 """ 139 | try: 140 | self.updateReserveInfo() 141 | resp = self.postOperation(Operate.getList) 142 | task_id = resp['data']['userAuth']['credit']['tasks'][0]['id'] 143 | 144 | task = Operate.doneTask 145 | task.value['variables']['user_task_id'] = task_id 146 | 147 | resp = self.postOperation(task) 148 | done = resp['data']['userAuth']['credit']['done'] 149 | 150 | if done: self.logs.putInfo("签到成功,已获取 5 积分") 151 | # else: self.logs.log("今日已签到,明天再来吧") 152 | except Exception as e: 153 | self.logs.putInfo(f"签到错误:{e}") 154 | 155 | 156 | def updateReserveInfo(self): 157 | """ 查询座位相关信息,并保存至日志工具中 """ 158 | 159 | # self.postOperation(Operate.getUserCancleConfig) 160 | info = self.postOperation(Operate.index) 161 | # 如果无法通过认证则重新获取授权码 162 | if 'errors' in info and info['errors'][0]['code'] == 40001: 163 | self.logs.putInfo("临时授权码过期,正在重新获取...") 164 | self.gainCookie() 165 | 166 | try: 167 | self.currentuser = info['data']['userAuth']['currentUser'] # 用户信息 168 | self.often_seats = info['data']['userAuth']['oftenseat']['list'] # 常用座位 169 | self.reserveinfo = info['data']['userAuth']['reserve']['reserve' ] # 预约信息 170 | self.stoken = info['data']['userAuth']['reserve']['getSToken'] # 退座令牌 171 | self.lib_id = self.reserveinfo['lib_id' ] # 场馆 ID 172 | self.lib_name = self.reserveinfo['lib_name' ] # 场馆名称 173 | self.seat_key = self.reserveinfo['seat_key' ] # 座位 ID 174 | self.seat_name = self.reserveinfo['seat_name'] # 座位号码 175 | self.status = self.reserveinfo['status' ] # 座位状态 176 | self.exp_date = self.reserveinfo['exp_date' ] # 过期时间 177 | self.diff_str = self.reserveinfo['diff_str' ] # 在馆时长 178 | self.isresrved = True # 若 userAuth.reserve 信息均正确则说明已经预约座位 179 | except Exception as e: 180 | # self.logs.putLog(f"当前无预约信息:{e}") 181 | self.isresrved = False 182 | 183 | #更新信息后将信息收集到 jar 包中 184 | status = ["未预约", "未预约", "待签到", "在馆", "暂离"] 185 | info_jar = { 186 | # "name": self.currentuser.get("user_student_name"), 187 | "name": self.Name, 188 | "email": self.Email, 189 | "lib_name": self.lib_name, 190 | "seat_name":self.seat_name, 191 | "status": status[self.status], 192 | "diff_str": self.diff_str, 193 | "cook_jar": self.cook_jar 194 | } 195 | self.logs.setSeatInfo(info_jar) 196 | 197 | return info_jar 198 | 199 | 200 | def reserveSeat(self, goalseat=None): 201 | """ 预约常用座位,默认预约常用座位 202 | theseat=[lib_id, seat_key] 203 | """ 204 | self.updateReserveInfo() 205 | 206 | # 若未指定座位则选择常用座位 207 | if goalseat==None: 208 | isovertime = isOverTime([7, 1, 0]) 209 | # 若常用座位不空闲,则切换另一常用座位 # 座位锁定:status=1 210 | oftenseat = self.often_seats[self.SeatID-1] 211 | if oftenseat['status']==1 and isovertime: 212 | oftenseat = self.often_seats[2-self.SeatID] 213 | 214 | lib_id = oftenseat['lib_id' ] 215 | seat_key = oftenseat['seat_key'] 216 | 217 | else: 218 | lib_id = goalseat[0] 219 | seat_key = goalseat[1] 220 | 221 | # 当前没有预约座位,则开始预约 222 | if (not self.isresrved): 223 | seat = Operate.reserveSeat 224 | seat.value['variables']['libId' ] = lib_id 225 | seat.value['variables']['seatKey'] = seat_key 226 | 227 | self.logs.putInfo(f"♻️ 开始预约:{seat_key}") 228 | resp = self.postOperation(seat) 229 | 230 | if 'errors' in resp: 231 | error = resp['errors'][0]['msg' ] 232 | ercod = resp['errors'][0]['code'] 233 | self.logs.putInfo(f"预约失败:{error} [{ercod}]" ) 234 | #错误码1:重复预约,错误码2:场馆关闭 235 | self.logs.setSubject(f"🚫 图书馆预约失败:{error}") 236 | return False 237 | 238 | else: 239 | self.updateReserveInfo() 240 | lib_name, seat_name = self.lib_name, self.seat_name 241 | self.logs.putInfo(f"预约成功:{lib_name} {seat_name}") 242 | self.logs.setSubject(f"🟢 图书馆预约成功:{lib_name} {seat_name}号") 243 | return True 244 | 245 | else: return True 246 | 247 | 248 | def cancelSeat(self): 249 | """ 主动退座 """ 250 | 251 | self.updateReserveInfo() 252 | 253 | if (self.isresrved): 254 | cancel = Operate.reserveCancle 255 | cancel.value['variables']['sToken'] = self.stoken 256 | self.postOperation(cancel) 257 | self.updateReserveInfo() 258 | 259 | if not self.isresrved: 260 | self.isresrved = False 261 | self.logs.putInfo(f"退座成功,本次学习时长:{self.diff_str}") 262 | return True 263 | else: 264 | self.logs.putInfo("退座失败,未知错误") 265 | return False 266 | 267 | else: return True 268 | 269 | 270 | def reserveTimer(self, ontime=[7,0,0]): 271 | """ 早上 7 点准点预约,默认预约一号常用位 """ 272 | 273 | self.updateReserveInfo() 274 | 275 | if not self.isresrved: 276 | sched = TimerThread() 277 | sched.addjob(ontime, self.reserveSeat) 278 | sched.addjob(ontime, self.reserveSeat) 279 | sched.addjob(ontime, self.reserveSeat) 280 | 281 | 282 | 283 | def reserveRenew(self, ontime:list): 284 | """ 若在指定时间前未签到,则重新预约 """ 285 | 286 | self.updateReserveInfo() 287 | 288 | intime = timeRange(ontime, 5) 289 | renew = self.status==2 290 | 291 | # status:未预约None,未签到2,在馆3,暂离4 292 | if intime and renew: 293 | self.cancelSeat() 294 | self.reserveSeat([self.lib_id, self.seat_key]) 295 | 296 | self.updateReserveInfo() 297 | if (self.isresrved): 298 | self.logs.putInfo("预测当前无法签到,已自动续约") 299 | self.logs.setSubject("♻️ 图书馆预约超时,已自动续约") 300 | else: 301 | self.logs.setSubject("🍁 图书馆预约超时,续约失败了") 302 | 303 | 304 | def reserveCancel(self, ontime:list): 305 | """ 若指定时间前仍无法签到,则取消预约 """ 306 | 307 | self.updateReserveInfo() 308 | 309 | intime = timeRange(ontime, 5) 310 | cancel = self.status==2 311 | 312 | # status:未预约None,未签到2,在馆3,暂离4 313 | if intime and cancel: 314 | # 若是系统全程自动续约,过期时间应该相差5min内 315 | if abs(time.time() - self.exp_date) < 300: 316 | self.cancelSeat() 317 | self.logs.putInfo("预测无法签到,故取消预约") 318 | self.logs.setSubject("🍁 图书馆预约再次超时,已取消预约") 319 | 320 | 321 | def closeCancel(self, ontime=[22,0,0]): 322 | """ 晚上 22点 闭馆 自动退座 """ 323 | 324 | self.updateReserveInfo() 325 | 326 | intime = timeRange(ontime, 5) 327 | if intime and self.isresrved: 328 | self.cancelSeat() 329 | self.logs.setSubject("🍀 22点闭馆,已自动退座") 330 | 331 | 332 | def outTimeCancel(self): 333 | """ 预约超时前5分钟 自动退座 """ 334 | 335 | self.updateReserveInfo() 336 | isTimeout = self.exp_date - int(time.time()) < 5 * 60 337 | needCancel = self.status!=3 # 3为不在馆 338 | 339 | if self.isresrved and isTimeout and needCancel: 340 | self.reerveCancel() 341 | self.logs.putInfo("签到时间不足5分钟,已取消预约") 342 | self.logs.setSubject("🍁 图书馆座位超时,已自动取消") 343 | return True 344 | else: 345 | return False 346 | -------------------------------------------------------------------------------- /utils/loginfo.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | class LogInfo(): 5 | """ 日志信息控制类 """ 6 | 7 | 8 | def __init__(self, Email=None, Name=None): 9 | """ Email: 接收人的邮箱 """ 10 | self.Name = Name 11 | self.Email = Email 12 | self.subject = None 13 | self.info_jar = {} #信息包,对外暴露 14 | self.seatinfo = {} 15 | self.__infolst = [] #用于收集发送信息 16 | self.__logslst = [] #用于收集差错日志 17 | 18 | 19 | def getInfoJar(self): 20 | """ 返回字典形式的日志信息包 """ 21 | 22 | content = '\n'.join(self.__infolst) 23 | alllogs = '\n'.join(self.__logslst) 24 | 25 | # 在用户信息包的基础上增加日志信息 26 | self.info_jar['subject'] = self.subject 27 | self.info_jar['content'] = content 28 | self.info_jar['alllogs'] = alllogs 29 | 30 | return self.info_jar 31 | 32 | 33 | def putInfo(self, info:str): 34 | """ 收集运行过程中的日志 """ 35 | 36 | times = datetime.datetime.now().strftime('%H:%M:%S.%f')[:12] 37 | log_item = times + " " + info 38 | self.__infolst.append(log_item) 39 | self.__logslst.append(log_item) 40 | 41 | 42 | def putLogs(self, info:str): 43 | """ 收集日志,非邮件发送用途""" 44 | times = datetime.datetime.now().strftime('%H:%M:%S.%f')[:12] 45 | log_item = times + " " + info 46 | self.__logslst.append(log_item) 47 | 48 | 49 | def setSeatInfo(self, dicts:dict): 50 | """ 设置座位预约信息 """ 51 | self.info_jar = dicts 52 | 53 | 54 | def setSubject(self, subject:str): 55 | """ 设置运行日志的标题 """ 56 | self.subject = subject 57 | 58 | -------------------------------------------------------------------------------- /utils/myexcept.py: -------------------------------------------------------------------------------- 1 | 2 | class TokenError(Exception): 3 | 4 | def __init__(self, msg): 5 | self.message = msg -------------------------------------------------------------------------------- /utils/push.py: -------------------------------------------------------------------------------- 1 | from email.mime.multipart import MIMEMultipart 2 | from config.config import user, passwd, servchan 3 | from email.mime.text import MIMEText 4 | from email.utils import formataddr 5 | from utils.weather import weather 6 | import requests 7 | import smtplib 8 | import time 9 | 10 | 11 | def sendEmail(email, subject, content): 12 | 13 | try: 14 | msg = MIMEMultipart() 15 | msg.attach(MIMEText(content, 'plain', 'utf-8')) 16 | msg['From' ] = formataddr(["预约机器人", user]) 17 | msg['to' ] = email 18 | msg['Subject'] = subject 19 | 20 | smtpser = smtplib.SMTP_SSL("smtp.qq.com", 465) 21 | smtpser.login(user, passwd) 22 | smtpser.sendmail(user, email, msg.as_string()) 23 | smtpser.quit() 24 | 25 | except Exception as e: 26 | print(f'🟠 邮件 {email} 发送失败:{e}' ) 27 | 28 | 29 | def privatePush(email, subject, content): 30 | """ 紧急推送微信消息至手机 """ 31 | 32 | url = f"https://sctapi.ftqq.com/{servchan}.send" 33 | data = {'text': subject, 'desp': content} 34 | requests.post(url, data=data) 35 | 36 | 37 | def weatherinfo()->str: 38 | """ 获取今日天气信息 """ 39 | dates = time.strftime("%y-%m-%d", time.localtime()) 40 | hours = int(time.strftime("%H", time.localtime())) 41 | skycon, description, temperature, probability = weather() 42 | 43 | if hours>0 and hours<11: 44 | greet = "早上好" 45 | elif hours>=11 and hours<18: 46 | greet = "下午好" 47 | else: 48 | greet = "晚上好" 49 | 50 | greet = f"{greet},今天是 {dates}" 51 | 52 | descrip1 = f"天气:{description}" 53 | descrip2 = f"温度:{temperature},未来半小时降雨量:{probability} mm/h" 54 | 55 | return f"{greet}\n{descrip1}\n{descrip2}" -------------------------------------------------------------------------------- /utils/synfile.py: -------------------------------------------------------------------------------- 1 | from library.consts import LOCAL_PATH, JIAN_PATH, PATH_PAIR1, PATH_PAIR2 2 | from config.config import webdav_usr, webdav_pwd 3 | from webdav4.client import Client 4 | 5 | url = "https://dav.jianguoyun.com/dav/" 6 | 7 | class WebDavCookies(): 8 | 9 | def up_cookies(path_pair=PATH_PAIR1): 10 | try: 11 | client = Client(base_url=url, auth=(webdav_usr, webdav_pwd)) 12 | client.upload_file(from_path=path_pair[0], to_path=path_pair[1], overwrite=True) 13 | return True 14 | except: 15 | return False 16 | 17 | def down_cookies(path_pair=PATH_PAIR1): 18 | try: 19 | client = Client(base_url=url, auth=(webdav_usr, webdav_pwd)) 20 | client.download_file(from_path=path_pair[1], to_path=path_pair[0]) 21 | return True 22 | except: 23 | return False -------------------------------------------------------------------------------- /utils/utils.py: -------------------------------------------------------------------------------- 1 | from utils.synfile import WebDavCookies 2 | from threading import Thread 3 | import datetime 4 | import time 5 | import json 6 | import os 7 | 8 | 9 | class TimerThread: 10 | """ 定时执行类,允许前后10分钟的误差内执行 11 | - 例如 定时执行 函数 hello("你好","中国") 12 | 13 | ``` spect = [8, 30, 0] 14 | t = TimerThread() 15 | t.add_job(spect, hello, "你好", "中国") 16 | ``` 17 | """ 18 | def __circlerun(self, spect, func, *args): 19 | spctime = timeFormat(spect) 20 | inrange = timeRange(spect, 10) 21 | # 允许指定时间的前后10分钟内执行 22 | if inrange: 23 | while True: 24 | nowtime = datetime.datetime.now() 25 | if spctime<=nowtime: 26 | func(*args) 27 | otm = f'{nowtime}' 28 | return otm 29 | 30 | def addjob(self, spectime:str, func, *args): 31 | """ 32 | - timeee: 指定时间,如 [08,30,00] 33 | - func: 函数名称 34 | - args: 函数 func 的参数 35 | """ 36 | # self.__circlerun(spectime, func, *args) 37 | t = Thread(target=self.__circlerun, args=(spectime, func, *args)) 38 | t.start() 39 | t.join() 40 | 41 | 42 | def writeJson(path_pair, file): 43 | with open(path_pair[0], "w") as f: 44 | json.dump(file, f) 45 | WebDavCookies.up_cookies(path_pair) 46 | 47 | 48 | def openJson(path)-> dict: 49 | try: 50 | with open(path, "r") as f: 51 | return json.load(f) 52 | except: 53 | return None 54 | 55 | 56 | def makePath(path: str): 57 | 58 | if not os.path.exists(path): 59 | os.makedirs(path) 60 | 61 | 62 | def timerRun(intime): 63 | """ 定时运行装饰器,定义函数时装饰指定时间 """ 64 | def wrapper(func): 65 | def deco(*args, **kwargs): 66 | while True: 67 | if intime <= time.strftime("%H:%M:%S", time.localtime()): 68 | func(*args, **kwargs) 69 | break 70 | return deco 71 | return wrapper 72 | 73 | 74 | def timeFormat(spect:list): 75 | today = datetime.datetime.now() 76 | timefmat = datetime.datetime(today.year, today.month, today.day, *spect) 77 | return timefmat 78 | 79 | 80 | def timeRange(spect:list, delta:int): 81 | """ 时间范围,[h, m, s, μs] 82 | - 如8点前后5分钟 83 | - spect=[8,0,0],delta=5 84 | - @return 是否在指定时间范围 85 | """ 86 | spctime = timeFormat(spect) 87 | nowtime = datetime.datetime.now() 88 | deltime = nowtime - spctime 89 | 90 | inrange = abs(deltime) < datetime.timedelta(minutes=delta) 91 | 92 | return inrange 93 | 94 | 95 | def isOverTime(spect:list): 96 | """ 是否超过某时间 97 | 时间范围,[h, m, s, μs] 98 | """ 99 | spctime = timeFormat(spect) 100 | nowtime = datetime.datetime.now() 101 | 102 | return nowtime > spctime -------------------------------------------------------------------------------- /utils/weather.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | url = "https://api.caiyunapp.com/v2.6/WcqmxGvXTxxwNAEa/118.64104528639601,32.082130970743954/hourly?hourlysteps=1" 5 | 6 | def weather(): 7 | resp = requests.get(url).text 8 | resp = json.loads(resp) 9 | resp = resp['result']['hourly'] 10 | 11 | description = resp['description'] # 天气描述 12 | skycon = resp['skycon'][0]['value'] # 天气情况 13 | temperature = resp['temperature'][0]['value'] # 室外温度 14 | probability = resp['precipitation'][0]['probability'] # 未来一小时降雨强度 15 | 16 | return skycon, description, temperature, probability --------------------------------------------------------------------------------