No course found!
" 75 | courses = self.ucommand["courses"] 76 | allc = [] 77 | 78 | try: 79 | for course in courses: 80 | allc.append( 81 | apilink( 82 | course, 83 | self.bid, 84 | self.url, 85 | self.usercheck, 86 | g_tformat=self.g_tformat, 87 | ) 88 | ) 89 | except Exception as e: 90 | raise Exception("invalid course", e) 91 | 92 | now_root = self.now.replace(hour=0, minute=0, second=0, microsecond=0) 93 | 94 | sem_begin = datetime.strptime(self.ucommand["semester_begin"], "%Y-%m-%d") 95 | 96 | bdays = (now_root - sem_begin).days 97 | bweeks = floor(bdays / 7) + 1 98 | 99 | if "title" in self.ucommand: 100 | self.print_own(f"{self.other["msg"]}
\n' 228 | 229 | def collect_assignment(self): 230 | self.cstate = "Assignment" 231 | asr = self.send(self.assignment) 232 | self.raw = asr 233 | self.ass_data = [] 234 | asr = json.loads(asr) 235 | for big in asr: 236 | a = big["assignments"] 237 | if a: 238 | for k in a: 239 | if k["due_at"]: 240 | dttime = datetime.strptime( 241 | k["due_at"], "%Y-%m-%dT%H:%M:%SZ" 242 | ) + timedelta(hours=8) 243 | if dttime < self.now: 244 | continue 245 | self.ass_data.append(k) 246 | elif k["updated_at"]: 247 | # Fallback to updated 248 | self.ass_data.append(k) 249 | self.ass_data.sort(key=self._cmp_ass, reverse=True) 250 | self.output = f"Warning: no output for course {self.cname} (id: {self.course})
" 358 | ) 359 | 360 | 361 | if __name__ == "__main__": 362 | # TEST 363 | cmgr = CanvasMGR() 364 | print(cmgr.get_response()) 365 | -------------------------------------------------------------------------------- /canvas_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from fastapi import FastAPI, Request, UploadFile 4 | from fastapi.responses import FileResponse 5 | from fastapi.middleware.cors import CORSMiddleware 6 | from config_mgr import ConfigMGR 7 | from canvas_mgr import CanvasMGR 8 | import urllib.parse 9 | from models import Position, Check, Course, URL 10 | from fastapi.responses import JSONResponse 11 | from os import path, listdir, remove, mkdir 12 | from updater import update 13 | import json 14 | import logging 15 | from typing import List 16 | 17 | """ 18 | Local function 19 | """ 20 | 21 | ALLOWED_EXTENSION = { 22 | "png", 23 | "jpg", 24 | "jpeg", 25 | "gif", 26 | "svg", 27 | "mp4", 28 | "mkv", 29 | "mov", 30 | "m4v", 31 | "avi", 32 | "wmv", 33 | "webm", 34 | } 35 | 36 | 37 | # INFO: Safety check for file 38 | def check_file(filename): 39 | base_path = "/public/res/" 40 | fullPath = path.normpath(path.join(base_path, filename)) 41 | if ( 42 | not "." in filename 43 | or not filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSION 44 | ): 45 | return "Illegal" 46 | if not fullPath.startswith(base_path): 47 | return "Illegal" 48 | else: 49 | return filename 50 | 51 | 52 | """ 53 | Canvas App 54 | 55 | This file contains all the APIs to access the 56 | configuration file/canvas backend, etc.. 57 | """ 58 | 59 | 60 | app = FastAPI(version="1.0.1", title="Canvas Helper", description="Canvas Helper API.") 61 | 62 | app.add_middleware( 63 | CORSMiddleware, 64 | allow_origins=["*"], 65 | allow_credentials=True, 66 | allow_methods=["*"], 67 | allow_headers=["*"], 68 | ) 69 | 70 | 71 | logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s") 72 | 73 | conf = ConfigMGR() 74 | 75 | # Self Update 76 | update() 77 | 78 | 79 | @app.get( 80 | "/config", 81 | summary="Get the configuration file", 82 | description="Get the configuration file.", 83 | tags=["config"], 84 | ) 85 | async def get_configuration(): 86 | return conf.get_conf() 87 | 88 | 89 | @app.get( 90 | "/config/refresh", 91 | tags=["config"], 92 | summary="Refresh the configuration file", 93 | description="Force to read the configuration file from disk.", 94 | ) 95 | async def refresh_conf(): 96 | conf.force_read() 97 | return JSONResponse(status_code=200, content={"message": "success"}) 98 | 99 | 100 | @app.get( 101 | "/config/key/{key}", 102 | tags=["config"], 103 | summary="Get a specific key from the configuration file", 104 | description="Get a specific key from the configuration file.", 105 | ) 106 | async def get_configuration_key(key: str): 107 | if key not in conf.get_conf(): 108 | return JSONResponse(status_code=404, content={"message": "Key not found"}) 109 | return conf.get_conf()[key] 110 | 111 | 112 | @app.put( 113 | "/config/key/{key}", 114 | tags=["config"], 115 | summary="Update a specific key in the configuration file", 116 | description="Update a specific key in the configuration file.", 117 | ) 118 | async def update_configuration(key: str, request: Request): 119 | body = await request.body() 120 | try: 121 | body_p = json.loads('{"data" : ' + body.decode(encoding="utf-8") + "}") 122 | except: 123 | return JSONResponse(status_code=400, content={"message": "Cannot parse body"}) 124 | conf.set_key_value(key, body_p["data"]) 125 | return JSONResponse(status_code=200, content={"message": "success"}) 126 | 127 | 128 | @app.delete( 129 | "/config/key/{key}", 130 | tags=["config"], 131 | summary="Delete a specific key in the configuration file", 132 | description="Delete a specific key in the configuration file.", 133 | ) 134 | async def delete_configuration(key: str): 135 | if key not in conf.get_conf(): 136 | return JSONResponse(status_code=404, content={"message": "Key not found"}) 137 | conf.remove_key(key) 138 | return JSONResponse(status_code=200, content={"message": "success"}) 139 | 140 | 141 | @app.get( 142 | "/config/verify", 143 | tags=["config"], 144 | summary="Verify the configuration file", 145 | description="Verify the configuration file.", 146 | ) 147 | async def verify_config(): 148 | """ 149 | Verify the configuration 150 | """ 151 | if "bid" not in conf.get_conf(): 152 | return JSONResponse(status_code=404, content={"message": "bid not found"}) 153 | if "url" not in conf.get_conf(): 154 | return JSONResponse(status_code=404, content={"message": "url not found"}) 155 | if "background_image" not in conf.get_conf() and "video" not in conf.get_conf(): 156 | return JSONResponse(status_code=400, content={"message": "background not set"}) 157 | # Test bid 158 | 159 | import requests 160 | 161 | headers = {"Authorization": f'Bearer {conf.get_conf()["bid"]}'} 162 | url = str(conf.get_conf()["url"]) 163 | if url.find("http://") != 0 and url.find("https://") != 0: 164 | # Invalid protocal 165 | url = "https://" + url 166 | conf.set_key_value("url", url) 167 | res = requests.get( 168 | urllib.parse.urljoin(url, "api/v1/accounts"), headers=headers 169 | ).status_code 170 | if res == 200: 171 | return JSONResponse(status_code=200, content={"message": "success"}) 172 | else: 173 | return JSONResponse(status_code=400, content={"message": "verification failed"}) 174 | 175 | 176 | @app.get( 177 | "/courses", 178 | tags=["course"], 179 | summary="Get all the courses", 180 | description="Get all the courses.", 181 | ) 182 | async def get_all_courses(): 183 | if "courses" not in conf.get_conf(): 184 | return [] 185 | return conf.get_conf()["courses"] 186 | 187 | 188 | @app.get( 189 | "/courses/canvas", 190 | tags=["course"], 191 | summary="Get all the courses from canvas", 192 | description="Get all the courses from canvas.", 193 | ) 194 | async def get_all_canvas_courses(): 195 | if "bid" not in conf.get_conf(): 196 | return JSONResponse(status_code=404, content={"message": "bid not found"}) 197 | 198 | import requests 199 | 200 | headers = {"Authorization": f'Bearer {conf.get_conf()["bid"]}'} 201 | res = requests.get( 202 | urllib.parse.urljoin( 203 | str(conf.get_conf()["url"]), "api/v1/dashboard/dashboard_cards" 204 | ), 205 | headers=headers, 206 | ).text 207 | return json.loads(res) 208 | 209 | 210 | @app.delete( 211 | "/courses/{course_id}", 212 | tags=["course"], 213 | summary="Delete a course", 214 | description="Delete a course. It will delete all the course items with the given course id.", 215 | ) 216 | async def delete_course(course_id: int): 217 | if "courses" not in conf.get_conf(): 218 | return JSONResponse(status_code=404, content={"message": "Courses not found"}) 219 | courses = conf.get_conf()["courses"] 220 | all_courses = [] 221 | if not isinstance(courses, List): 222 | return JSONResponse( 223 | status_code=404, content={"message": "Courses type should be list."} 224 | ) 225 | else: 226 | for course in courses: 227 | if course["course_id"] != course_id: 228 | all_courses.append(course) 229 | conf.set_key_value("courses", all_courses) 230 | return JSONResponse(status_code=200, content={"message": "success"}) 231 | 232 | 233 | @app.delete( 234 | "/courses/{course_id}/{type}", 235 | tags=["course"], 236 | summary="Delete a course item", 237 | description="Delete a course item. It will delete the course item with the given course id and type.", 238 | ) 239 | async def delete_course_item(course_id: int, type: str): 240 | if "courses" not in conf.get_conf(): 241 | return JSONResponse(status_code=404, content={"message": "Courses not found"}) 242 | courses = conf.get_conf()["courses"] 243 | all_courses = [] 244 | if not isinstance(courses, List): 245 | JSONResponse( 246 | status_code=404, content={"message": "Courses type should be list"} 247 | ) 248 | else: 249 | for course in courses: 250 | if course["course_id"] != course_id or course["type"] != type: 251 | all_courses.append(course) 252 | conf.set_key_value("courses", all_courses) 253 | return JSONResponse(status_code=200, content={"message": "success"}) 254 | 255 | 256 | @app.post( 257 | "/courses", tags=["course"], summary="Add a course", description="Add a course." 258 | ) 259 | async def create_course(course: Course): 260 | if course.type not in ["ann", "dis", "ass"]: 261 | return JSONResponse(status_code=400, content={"message": "Invalid course type"}) 262 | if course.name == "": 263 | return JSONResponse(status_code=400, content={"message": "Empty course name"}) 264 | course_info = { 265 | "course_id": course.id, 266 | "course_name": course.name, 267 | "type": course.type, 268 | "maxshow": course.maxshow, 269 | "order": course.order, 270 | "msg": course.msg, 271 | } 272 | if "courses" not in conf.get_conf(): 273 | ori_courses = [] 274 | else: 275 | ori_courses = conf.get_conf()["courses"] 276 | # Check if the course already exists 277 | if not isinstance(ori_courses, List): 278 | JSONResponse( 279 | status_code=404, content={"message": "Courses type should be list."} 280 | ) 281 | else: 282 | for c in ori_courses: 283 | if c["course_id"] == course.id and c["type"] == course.type: 284 | return JSONResponse( 285 | status_code=400, content={"message": "Course already exists"} 286 | ) 287 | ori_courses.append(course_info) 288 | conf.set_key_value("courses", ori_courses) 289 | return JSONResponse(status_code=200, content={"message": "success"}) 290 | 291 | 292 | @app.put( 293 | "/courses", 294 | tags=["course"], 295 | summary="Modify a course", 296 | description="Modify a course.", 297 | ) 298 | async def modify_course(index: int, course: Course): 299 | if "courses" not in conf.get_conf(): 300 | return JSONResponse(status_code=404, content={"message": "Courses not found"}) 301 | courses = conf.get_conf()["courses"] 302 | if not isinstance(courses, List): 303 | return JSONResponse( 304 | status_code=404, content={"message": "Courses type should be list"} 305 | ) 306 | if index >= len(courses) or index < 0: 307 | return JSONResponse(status_code=404, content={"message": "Course not found"}) 308 | if course.type not in ["ann", "ass", "dis"]: 309 | return JSONResponse(status_code=400, content={"message": "Invalid course type"}) 310 | if course.name == "": 311 | return JSONResponse(status_code=400, content={"message": "Empty course name"}) 312 | course_info = { 313 | "course_id": course.id, 314 | "course_name": course.name, 315 | "type": course.type, 316 | "maxshow": course.maxshow, 317 | "order": course.order, 318 | "msg": course.msg, 319 | } 320 | # Test if the course already exists 321 | for i in range(len(courses)): 322 | if ( 323 | i != index 324 | and courses[i]["course_id"] == course.id 325 | and courses[i]["type"] == course.type 326 | ): 327 | return JSONResponse( 328 | status_code=400, content={"message": "Course already exists"} 329 | ) 330 | 331 | courses[index] = course_info 332 | conf.set_key_value("courses", courses) 333 | return JSONResponse(status_code=200, content={"message": "success"}) 334 | 335 | 336 | @app.get( 337 | "/canvas/dashboard", 338 | tags=["canvas"], 339 | summary="Get the dashboard", 340 | description="Get the dashboard.", 341 | ) 342 | async def get_dashboard(cache: bool = False, mode: str = "html"): 343 | if cache: 344 | # Use cache 345 | if path.exists("./canvas/cache.json"): 346 | with open( 347 | "./canvas/cache.json", "r", encoding="utf-8", errors="ignore" 348 | ) as f: 349 | obj = json.load(f) 350 | if mode == "html": 351 | return {"data": obj["html"]} 352 | elif mode == "json": 353 | return {"data": obj["json"]} 354 | else: 355 | return JSONResponse( 356 | status_code=400, content={"message": "Mode not supported"} 357 | ) 358 | else: 359 | return JSONResponse(status_code=404, content={"message": "Cache not found"}) 360 | # No cache 361 | canvas = CanvasMGR(mode) 362 | return {"data": canvas.get_response()} 363 | 364 | 365 | @app.post( 366 | "/canvas/check/{name}", 367 | tags=["canvas"], 368 | summary="Check some task", 369 | description="Check some task.", 370 | ) 371 | async def set_check(name: str, check: Check): 372 | """ 373 | Check 374 | 375 | Only 1,2,3 is available 376 | """ 377 | if check.type < 0 or check.type > 3: 378 | return JSONResponse(status_code=400, content={"message": "Invalid check type"}) 379 | all_checks = [{"name": name, "type": check.type}] 380 | if "checks" in conf.get_conf(): 381 | ori_checks = conf.get_conf()["checks"] 382 | if not isinstance(ori_checks, List): 383 | return JSONResponse( 384 | status_code=404, content={"message": "Courses type should be list"} 385 | ) 386 | for ori_check in ori_checks: 387 | if ori_check["name"] != name: 388 | all_checks.append(ori_check) 389 | conf.set_key_value("checks", all_checks) 390 | return JSONResponse(status_code=200, content={"message": "success"}) 391 | 392 | 393 | @app.get( 394 | "/canvas/position", 395 | tags=["canvas"], 396 | summary="Get the position", 397 | description="Get the position.", 398 | ) 399 | async def get_position(): 400 | """ 401 | Get position 402 | """ 403 | if "position" not in conf.configuration: 404 | return JSONResponse(status_code=404, content={"message": "Position not found"}) 405 | return conf.get_conf()["position"] 406 | 407 | 408 | @app.put( 409 | "/canvas/position", 410 | tags=["canvas"], 411 | summary="Set the position", 412 | description="Set the position.", 413 | ) 414 | async def update_position(position: Position): 415 | """ 416 | Set position 417 | """ 418 | conf.set_key_value( 419 | "position", 420 | { 421 | "left": position.left, 422 | "top": position.top, 423 | "width": position.width, 424 | "height": position.height, 425 | }, 426 | ) 427 | return JSONResponse(status_code=200, content={"message": "success"}) 428 | 429 | 430 | @app.post( 431 | "/file/upload", 432 | tags=["file"], 433 | summary="Upload file", 434 | description="Upload file to public/res.", 435 | ) 436 | async def upload_file(file: UploadFile): 437 | if not path.exists("./public/res"): 438 | mkdir("./public/res") 439 | tmp = check_file(file.filename) 440 | if tmp == "Illegal": 441 | return JSONResponse(status_code=404, content={"message": "Illegal file name"}) 442 | with open(f"./public/res/{file.filename}", "wb") as out_file: 443 | out_file.write(file.file.read()) 444 | return JSONResponse(status_code=200, content={"message": "success"}) 445 | 446 | 447 | @app.delete( 448 | "/file", 449 | tags=["file"], 450 | summary="Delete file", 451 | description="Delete file in public/res.", 452 | ) 453 | async def delete_file(name: str): 454 | tmp = check_file(name) 455 | if tmp == "Illegal": 456 | return JSONResponse(status_code=404, content={"message": "Illegal file name"}) 457 | if path.exists(f"./public/res/{name}"): 458 | remove(f"./public/res/{name}") 459 | return JSONResponse(status_code=200, content={"message": "success"}) 460 | else: 461 | return JSONResponse(status_code=404, content={"message": "File not found"}) 462 | 463 | 464 | @app.get( 465 | "/file", 466 | tags=["file"], 467 | summary="Get file list", 468 | description="Get files in public/res.", 469 | ) 470 | async def get_file_list(): 471 | if path.exists("./public/res"): 472 | return {"files": listdir("./public/res")} 473 | else: 474 | mkdir("./public/res") 475 | return {"files": []} 476 | 477 | 478 | @app.get( 479 | "/file/{name}", 480 | tags=["file"], 481 | summary="Get file", 482 | description="Get file in public/res.", 483 | ) 484 | async def get_file(name: str): 485 | if path.exists(f"./public/res/{name}"): 486 | return FileResponse(f"./public/res/{name}") 487 | else: 488 | return JSONResponse(status_code=404, content={"message": "File not found"}) 489 | 490 | 491 | @app.post( 492 | "/browser", 493 | tags=["misc"], 494 | summary="Open URL in web browser", 495 | description="Open URL in web browser.", 496 | ) 497 | async def open_url(data: URL): 498 | import webbrowser 499 | 500 | try: 501 | if data.browser: 502 | res = webbrowser.get(data.browser).open(data.url) 503 | else: 504 | res = webbrowser.open(data.url) 505 | if not res: 506 | raise Exception("Cannot find web browser") 507 | return JSONResponse(status_code=200, content={"message": "Opened"}) 508 | except Exception as e: 509 | logging.warning(e) 510 | return JSONResponse(status_code=400, content={"message": "Failed to open"}) 511 | --------------------------------------------------------------------------------