TronClass Copilot

Your best copilot for TronClass

当前为 2024-05-06 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name TronClass Copilot
  3. // @namespace Anong0u0
  4. // @version 0.0.8
  5. // @description Your best copilot for TronClass
  6. // @author Anong0u0
  7. // @match https://eclass.yuntech.edu.tw/*
  8. // @match https://elearning.aeust.edu.tw/*
  9. // @match https://elearn.ntuspecs.ntu.edu.tw/*
  10. // @match https://elearn2.fju.edu.tw/*
  11. // @match https://iclass.tku.edu.tw/*
  12. // @match https://tronclass.ntou.edu.tw/*
  13. // @match https://tronclass.hk.edu.tw/*
  14. // @match https://tronclass.kh.usc.edu.tw/*
  15. // @match https://tronclass.usc.edu.tw/*
  16. // @match https://tronclass.cyut.edu.tw/*
  17. // @match https://tronclass.ypu.edu.tw/*
  18. // @match https://tccas.thu.edu.tw/*
  19. // @match https://ilearn.thu.edu.tw:8080/*
  20. // @match https://tronclass.cjcu.edu.tw/*
  21. // @match https://tronclass.asia.edu.tw/*
  22. // @match https://tronclass.mdu.edu.tw/*
  23. // @match https://ulearn.nfu.edu.tw/*
  24. // @match https://tc.nutc.edu.tw/*
  25. // @match https://tronclass.au.edu.tw/*
  26. // @match https://tronclass.cgust.edu.tw/*
  27. // @match https://tronclass.ocu.edu.tw/*
  28. // @match https://tc.stu.edu.tw/*
  29. // @match https://tronclass.ctust.edu.tw/*
  30. // @match https://tronclass.pu.edu.tw/*
  31. // @match https://nou.tronclass.com.tw/*
  32. // @match https://tronclass.must.edu.tw/*
  33. // @match https://tronclass.scu.edu.tw/*
  34. // @match https://ilearn.ttu.edu.tw/*
  35. // @match https://iclass.hwu.edu.tw/*
  36. // @icon https://tronclass.com.tw/static/assets/images/favicon-b420ac72.ico
  37. // @grant GM_xmlhttpRequest
  38. // @run-at document-start
  39. // @license Beerware
  40. // ==/UserScript==
  41.  
  42. const _parse = JSON.parse
  43. const parseSet = {allow_download: true, allow_forward_seeking: true, pause_when_leaving_window: false}
  44. JSON.parse = (text, reviver) => _parse(text, (k, v) => {
  45. if(reviver) v = reviver(k, v)
  46. return parseSet[k] || v
  47. })
  48.  
  49. const _addEventListener = Window.prototype.addEventListener;
  50. Window.prototype.addEventListener = (eventName, fn, options) => {
  51. if (eventName === "blur" && String(fn).match(/pause/)) return;
  52. _addEventListener(eventName, fn, options);
  53. };
  54.  
  55. const delay = (ms = 0) => {return new Promise((r)=>{setTimeout(r, ms)})}
  56.  
  57. const waitElementLoad = (elementSelector, selectCount = 1, tryTimes = 1, interval = 0) =>
  58. {
  59. return new Promise(async (resolve, reject)=>
  60. {
  61. let t = 1, result;
  62. while(true)
  63. {
  64. if(selectCount != 1) {if((result = document.querySelectorAll(elementSelector)).length >= selectCount) break;}
  65. else {if(result = document.querySelector(elementSelector)) break;}
  66.  
  67. if(tryTimes>0 && ++t>tryTimes) return reject(new Error("Wait Timeout"));
  68. await delay(interval);
  69. }
  70. resolve(result);
  71. })
  72. }
  73.  
  74. const requests = ({method, url, type = "", data = null, headers = {}}) => {
  75. return new Promise(async (resolve, reject) => {
  76. GM_xmlhttpRequest({
  77. method: method,
  78. url: url,
  79. headers: headers,
  80. responseType: type,
  81. data: data,
  82. onload: resolve,
  83. onerror: reject,
  84. onabort: reject
  85. });
  86. });
  87. };
  88.  
  89. Node.prototype.catch = function ()
  90. {
  91. const a = document.createElement("a")
  92. a.target = "_blank"
  93. this.insertAdjacentElement("beforebegin", a)
  94. a.appendChild(this)
  95. return a
  96. }
  97.  
  98. const css = document.createElement("style")
  99. css.innerHTML = `
  100. .title,
  101. .forum-category-title,
  102. .group-set > span
  103. {color:var(--primary-text-color)}
  104. `
  105.  
  106. const embedLink = async () =>
  107. {
  108. const courseID = location.href.match(/(?<=course\/)\d+/)
  109. document.body.appendChild(css)
  110. if (location.href.match(/learning-activity\/full-screen/))
  111. {
  112. const dict = {};
  113. (await requests({method:"get", url:`/api/courses/${courseID}/activities`,type:"json"}))
  114. .response.activities.forEach((e)=>{dict[e.title] = e.type=="questionnaire"?`questionnaire/${e.id}`:e.id});
  115. (await requests({method:"get", url:`/api/courses/${courseID}/exams`,type:"json"}))
  116. .response.exams.forEach((e)=>{dict[e.title] = `exam/${e.id}`});
  117. document.querySelectorAll(".activity a[ng-click]").forEach((e) =>
  118. {
  119. if(!(e.textContent.trim() in dict)) return;
  120. e.addEventListener("click", (e) => e.stopImmediatePropagation(), true)
  121. e.href = `/course/${courseID}/learning-activity/full-screen#/${dict[e.textContent.trim()]}`
  122. })
  123. }
  124. else if (location.href.match(/homework/))
  125. {
  126. const ids = (await requests({method:"get", url:`/api/courses/${courseID}/homework-activities?page_size=20`,type:"json"}))
  127. .response.homework_activities.map((e)=>e.id).reverse()
  128. document.querySelectorAll(".list-item").forEach((e, idx)=>
  129. {
  130. e.querySelectorAll("[ng-click]").forEach((e)=>e.addEventListener("click", (e) => e.stopImmediatePropagation(), true))
  131. e.catch().href = `/course/${courseID}/learning-activity#/${ids[idx]}`
  132. })
  133. // TODO: 交作業
  134. }
  135. else if (location.href.match(/(?<!learning-activity#\/)exam/))
  136. {
  137. const ids = (await requests({method:"get", url:`/api/courses/${courseID}/exam-list?page_size=20`,type:"json"}))
  138. .response.exams.map((e)=>e.id).reverse()
  139. document.querySelectorAll(".sub-title").forEach((e, idx)=>
  140. {
  141. e.querySelector("[ng-click]").addEventListener("click", (e) => e.stopImmediatePropagation(), true)
  142. e.catch().href = `/course/${courseID}/learning-activity#/exam/${ids[idx]}`
  143. })
  144. }
  145. else if(location.href.match(/forum(?!#\/topic-category)/))
  146. {
  147. const ids = (await requests({method:"get", url:`/api/courses/${courseID}/topic-categories?page_size=20`,type:"json"}))
  148. .response.topic_categories.map((e)=>e.id).reverse()
  149. ids.unshift(ids.pop())
  150. document.querySelectorAll(".list-item").forEach((e, idx)=>
  151. {
  152. e.addEventListener("click", (e) => e.stopImmediatePropagation(), true);
  153. e.catch().href = `/course/${courseID}/forum#/topic-category/${ids[idx]}`
  154. })
  155. }
  156. else if(location.href.match(/forum#\/topic-category/))
  157. {
  158. const page = document.querySelector(".pager-button.active").innerText,
  159. count = document.querySelector(".last-page").innerText,
  160. ids = (await requests({method:"get", url:`/api/forum/categories/${location.href.match(/(?<=category\/)\d+/)}?page=${page}`,type:"json"}))
  161. .response.result.topics.map((e)=>e.id),
  162. idList = ids.join(",")
  163. document.querySelectorAll(".list-item a[ng-click]").forEach((e, idx)=>
  164. {
  165. e.addEventListener("click", (e) => e.stopImmediatePropagation(), true);
  166. e.href = `/course/${courseID}/forum#/topics/${ids[idx]}?topicIds=${idList}&pageIndex=${page}&pageCount=${count}&predicate=lastUpdatedDate&reverse`
  167. })
  168. }
  169. else if (location.href.match(/\/user\/index/))
  170. {
  171. const li = (await requests({method:"get", url:`/api/todos`,type:"json"}))
  172. .response.todo_list.sort((a,b)=>new Date(a.end_time)-new Date(b.end_time)).map((e)=>{return {cid:e.course_id, aid:e.id, type:e.type}})
  173. document.querySelectorAll(".todo-list > a").forEach((e, idx)=>
  174. {
  175. switch(li[idx].type)
  176. {
  177. case "homework":
  178. e.href = `/course/${li[idx].cid}/learning-activity#/${li[idx].aid}`
  179. break
  180. case "exam":
  181. e.href = `/course/${li[idx].cid}/learning-activity#/exam/${li[idx].aid}`
  182. break
  183. case "questionnaire":
  184. e.href = `/course/${li[idx].cid}/learning-activity/full-screen#/questionnaire/${li[idx].aid}`
  185. break
  186. }
  187. e.addEventListener("click", (e) => e.stopImmediatePropagation(), true)
  188. })
  189. }
  190.  
  191. document.querySelectorAll("[id^=learning-activity]").forEach((e) =>
  192. {
  193. const actID = e.id.match(/\d+/),
  194. collapse = e.querySelector(".expand-collapse-attachments")
  195. if(collapse) collapse.addEventListener("click", (e) => e.preventDefault())
  196. e.querySelectorAll(".attachment-row").forEach((e)=>e.addEventListener("click", (e) => e.preventDefault()))
  197. e.querySelector("[ng-click]").addEventListener("click", (e) => {
  198. if(!(e.target == collapse || e.target.closest(".attachment-row"))) e.stopImmediatePropagation();
  199. }, true);
  200. const type = e.querySelector("[ng-switch-when]").getAttribute("ng-switch-when"),
  201. a = e.catch()
  202. switch(type)
  203. {
  204. case "exam":
  205. a.href = `/course/${courseID}/learning-activity#/exam/${actID}`
  206. break
  207. case "web_link":
  208. requests({method:"get",url:`/api/activities/${actID}`,type:"json"}).then((r)=>{a.href = r.response.data.link})
  209. break
  210. case "homework":
  211. a.href = `/course/${courseID}/learning-activity#/${actID}`
  212. break
  213. case "questionnaire":
  214. a.href = `/course/${courseID}/learning-activity/full-screen#/questionnaire/${actID}`
  215. break
  216. case "material":
  217. case "online_video":
  218. case "forum":
  219. default:
  220. a.href = `/course/${courseID}/learning-activity/full-screen#/${actID}`
  221. break
  222. /* TODO:
  223. 'slide': '微課程',
  224. 'lesson': '錄影教材',
  225. 'lesson_replay': '教室录播',
  226. 'chatroom': 'iSlide 直播',
  227. 'classroom': '隨堂測驗',
  228. 'page': '頁面',
  229. 'scorm': '第三方教材',
  230. 'interaction': '互動教材',
  231. 'feedback': '教學回饋',
  232. 'virtual_classroom': 'Connect 直播',
  233. 'zoom': 'Zoom直播',
  234. 'microsoft_teams_meeting': 'Teams 直播',
  235. 'google_meeting': 'Google Live',
  236. 'webex_meeting': 'Webex 直播',
  237. 'welink': 'Welink',
  238. 'classin': 'ClassIn 直播',
  239. 'live_record': '直播',
  240. 'select_student': '選人',
  241. 'race_answer': '搶答',
  242. 'number_rollcall': '数字点名',
  243. 'qr_rollcall': '二维码点名',
  244. 'virtual_experiment': '模擬實驗',
  245. 'wecom_meeting': 'WeCom會議',
  246. 'vocabulary': '詞彙表',
  247. */
  248. }
  249.  
  250. })
  251. }
  252.  
  253. const videoSpeedrun = async (element) =>
  254. {
  255. const [userID, orgID] = st ? [st.userId, st.orgId] : (await requests({method:"get",url:"/api/profile",type:"json"}).then((e)=>[e.response.id, e.response.org.id])),
  256. courseID = st?.tags.course_id || location.href.match(/(?<=course\/)\d+/),
  257. actID = st?.tags.activity_id || location.href.split('/').pop(),
  258. endTime = element.innerText.split(":").map((e)=> Number(e)).reduce((acc, curr, index) => acc + curr * [3600,60,1][index], 0),
  259. need = Number(document.querySelector(".completion-criterion > .attribute-value").innerText.match(/\d+(?=%)/)),
  260. now = (await requests({method:"POST", url:`/api/course/activities-read/${actID}`, type:"json"})).response.data?.completeness || 0
  261.  
  262. await requests({ // increase student stat times for watch video
  263. method:"post",
  264. url:"/statistics/api/online-videos",
  265. data:`{"user_id":${userID},"org_id":${orgID},"course_id":${courseID},"activity_id":${actID},"action_type":"view","ts":${Date.now()}}`
  266. })
  267.  
  268. if(now >= need)
  269. {
  270. console.log("速刷已完成");
  271. return;
  272. }
  273.  
  274. const title = document.querySelector("span.title"),
  275. origText = title.innerText
  276. let res;
  277. for(let nowTime=Math.floor(endTime*0.01*Math.max(now-10,0)); nowTime!=endTime;)
  278. {
  279. const dur = endTime-nowTime<120 ? endTime-nowTime : Math.floor(120-Math.random()*66),
  280. newTime = nowTime + dur
  281. res = await requests({ // watch video api
  282. method: "POST",
  283. url: `/api/course/activities-read/${actID}`,
  284. data: `{"start":${nowTime},"end":${newTime}}`,
  285. headers: {"Content-Type": "application/json"},
  286. type: "json"
  287. }).catch(()=>alert("速刷失敗,請聯絡作者"))
  288.  
  289. await requests({ // increase student stat video watching time
  290. method:"post",
  291. url:"/statistics/api/online-videos",
  292. data:`{"user_id":${userID},"org_id":${orgID},"course_id":${courseID},"activity_id":${actID},"action_type":"play","ts":${Date.now()},"start_at":${nowTime},"end_at":${newTime},"duration":${dur}}`
  293. })
  294. await requests({ // increase student stat times for access course
  295. method:"post",
  296. url:"/statistics/api/user-visits",
  297. data:`{"user_id":"${userID}","org_id":${orgID},"course_id":"${courseID}","visit_duration":${dur}}`
  298. })
  299.  
  300. console.log(`${res.response.data.completeness}% ${nowTime}-${newTime} (${endTime})`)
  301. title.innerHTML = `<b>[速刷中] (${res.response.data.completeness}%) ${newTime}/${endTime}s</b> ${origText}`
  302. nowTime = newTime
  303. }
  304. await delay(100)
  305. if(res.response.data.completeness < need) alert("速刷失敗,請聯絡作者")
  306. location.reload()
  307. }
  308.  
  309. const myStat = async () =>
  310. {
  311. const courseID = location.href.match(/(?<=course\/)\d+/)
  312.  
  313. }
  314.  
  315. let lock = false;
  316. waitElementLoad("#ngProgress",1,0,50).then((e)=>{
  317. new MutationObserver(()=>{
  318. if(lock==false && e.style.width=="100%")
  319. {
  320. lock = true
  321. embedLink()
  322. waitElementLoad("span[ng-bind='ui.duration|formatTime']").then(videoSpeedrun).catch(()=>{})
  323. if(location.href.match(/course\/\d+/)) myStat()
  324. }
  325. else if (e.style.width=="0%") lock = false;
  326. }).observe(e, {attributes:true})
  327. })
  328. waitElementLoad("[data-category=tronclass-footer]",1,10,200).then((e)=>e.remove())
  329.  
  330.  
  331.  

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址