TronClass Copilot

Your best copilot for TronClass

目前为 2024-11-30 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name TronClass Copilot
  3. // @namespace Anong0u0
  4. // @version 0.0.11.1
  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?conditions=%7B%22itemsSortBy%22:%7B%22predicate%22:%22module%22,%22reverse%22:false%7D%7D&page_size=20`,type:"json"}))
  127. .response.homework_activities.map((e)=>e.id)
  128. document.querySelectorAll(".list-item").forEach((e, idx)=>
  129. {
  130. e.querySelectorAll("[ng-click]").forEach((e)=>{
  131. if(!e.closest(".activity-operations-container")) e.addEventListener("click", (e) => e.stopImmediatePropagation(), true)
  132. })
  133. e.catch().href = `/course/${courseID}/learning-activity#/${ids[idx]}`
  134. })
  135. }
  136. else if (location.href.match(/(?<!learning-activity#\/)exam/))
  137. {
  138. const ids = (await requests({method:"get", url:`/api/courses/${courseID}/exam-list?page_size=20`,type:"json"}))
  139. .response.exams.map((e)=>e.id).reverse()
  140. document.querySelectorAll(".sub-title").forEach((e, idx)=>
  141. {
  142. e.querySelector("[ng-click]").addEventListener("click", (e) => e.stopImmediatePropagation(), true)
  143. e.catch().href = `/course/${courseID}/learning-activity#/exam/${ids[idx]}`
  144. })
  145. }
  146. else if(location.href.match(/forum(?!#\/topic-category)/))
  147. {
  148. const ids = (await requests({method:"get", url:`/api/courses/${courseID}/topic-categories?page_size=20`,type:"json"}))
  149. .response.topic_categories.map((e)=>e.id).reverse()
  150. ids.unshift(ids.pop())
  151. document.querySelectorAll(".list-item").forEach((e, idx)=>
  152. {
  153. e.addEventListener("click", (e) => e.stopImmediatePropagation(), true);
  154. e.catch().href = `/course/${courseID}/forum#/topic-category/${ids[idx]}`
  155. })
  156. }
  157. else if(location.href.match(/forum#\/topic-category/))
  158. {
  159. const sort = document.querySelector(".sort-operation .sort-active"),
  160. isReversed = sort.className.match(/down/) ? true : false,
  161. reversed = isReversed ? "&reverse" : "",
  162. sortTypeName = sort.parentElement.parentElement.className,
  163. sortType = {"last-updated-time":"lastUpdatedDate", "replies-number":"reply_count", "like-count":"like_count"}[
  164. ["last-updated-time", "replies-number", "like-count"].filter((e)=>sortTypeName.match(e))[0]],
  165. page = document.querySelector(".pager-button.active").innerText,
  166. count = document.querySelector(".last-page").innerText.trim(),
  167. ids = (await requests({method:"get", url:`/api/forum/categories/${location.href.match(/(?<=category\/)\d+/)}?page=${page}&conditions=${
  168. encodeURIComponent(`{"topic_sort_by":{"predicate":"${sortType}","reverse":${String(isReversed)}}}`)}`,type:"json"}))
  169. .response.result.topics.map((e)=>e.id),
  170. idList = ids.join(",")
  171. document.querySelectorAll(".topic-summary a[ng-click]").forEach((e, idx)=>
  172. {
  173. e.addEventListener("click", (e) => e.stopImmediatePropagation(), true);
  174. e.href = `/course/${courseID}/forum#/topics/${ids[idx]}?topicIds=${idList}&pageIndex=${page}&pageCount=${count}&predicate=${sortType}${reversed}`
  175. })
  176. }
  177. else if (location.href.match(/\/user\/index/))
  178. {
  179. const li = (await requests({method:"get", url:`/api/todos`,type:"json"}))
  180. .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}})
  181. document.querySelectorAll(".todo-list > a").forEach((e, idx)=>
  182. {
  183. switch(li[idx].type)
  184. {
  185. case "homework":
  186. e.href = `/course/${li[idx].cid}/learning-activity#/${li[idx].aid}`
  187. break
  188. case "exam":
  189. e.href = `/course/${li[idx].cid}/learning-activity#/exam/${li[idx].aid}`
  190. break
  191. case "questionnaire":
  192. e.href = `/course/${li[idx].cid}/learning-activity/full-screen#/questionnaire/${li[idx].aid}`
  193. break
  194. }
  195. e.addEventListener("click", (e) => e.stopImmediatePropagation(), true)
  196. })
  197. }
  198.  
  199. document.querySelectorAll("[id^=learning-activity]").forEach((e) =>
  200. {
  201. const actID = e.id.match(/\d+/),
  202. collapse = e.querySelector(".expand-collapse-attachments")
  203. if(collapse) collapse.addEventListener("click", (e) => e.preventDefault())
  204. e.querySelectorAll(".attachment-row").forEach((e)=>e.addEventListener("click", (e) => e.preventDefault()))
  205. e.querySelector("[ng-click]").addEventListener("click", (e) => {
  206. if(!(e.target == collapse || e.target.closest(".attachment-row"))) e.stopImmediatePropagation();
  207. }, true);
  208. const type = e.querySelector("[ng-switch-when]").getAttribute("ng-switch-when"),
  209. a = e.catch()
  210. switch(type)
  211. {
  212. case "exam":
  213. a.href = `/course/${courseID}/learning-activity#/exam/${actID}`
  214. break
  215. case "web_link":
  216. requests({method:"get",url:`/api/activities/${actID}`,type:"json"}).then((r)=>{a.href = r.response.data.link})
  217. break
  218. case "homework":
  219. a.href = `/course/${courseID}/learning-activity#/${actID}`
  220. break
  221. case "questionnaire":
  222. a.href = `/course/${courseID}/learning-activity/full-screen#/questionnaire/${actID}`
  223. break
  224. case "material":
  225. case "online_video":
  226. case "forum":
  227. default:
  228. a.href = `/course/${courseID}/learning-activity/full-screen#/${actID}`
  229. break
  230. /* TODO:
  231. 'slide': '微課程',
  232. 'lesson': '錄影教材',
  233. 'lesson_replay': '教室录播',
  234. 'chatroom': 'iSlide 直播',
  235. 'classroom': '隨堂測驗',
  236. 'page': '頁面',
  237. 'scorm': '第三方教材',
  238. 'interaction': '互動教材',
  239. 'feedback': '教學回饋',
  240. 'virtual_classroom': 'Connect 直播',
  241. 'zoom': 'Zoom直播',
  242. 'microsoft_teams_meeting': 'Teams 直播',
  243. 'google_meeting': 'Google Live',
  244. 'webex_meeting': 'Webex 直播',
  245. 'welink': 'Welink',
  246. 'classin': 'ClassIn 直播',
  247. 'live_record': '直播',
  248. 'select_student': '選人',
  249. 'race_answer': '搶答',
  250. 'number_rollcall': '数字点名',
  251. 'qr_rollcall': '二维码点名',
  252. 'virtual_experiment': '模擬實驗',
  253. 'wecom_meeting': 'WeCom會議',
  254. 'vocabulary': '詞彙表',
  255. */
  256. }
  257.  
  258. })
  259. }
  260.  
  261. const videoSpeedrun = async (element) =>
  262. {
  263. 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])),
  264. courseID = st?.tags.course_id || location.href.match(/(?<=course\/)\d+/),
  265. actID = st?.tags.activity_id || location.href.split('/').pop(),
  266. endTime = element.innerText.split(":").map((e)=> Number(e)).reduce((acc, curr, index) => acc + curr * [3600,60,1][index], 0),
  267. need = Number(document.querySelector(".completion-criterion > .attribute-value").innerText.match(/\d+(?=%)/)),
  268. now = (await requests({method:"POST", url:`/api/course/activities-read/${actID}`, type:"json"})).response.data?.completeness || 0
  269.  
  270. await requests({ // increase student stat times for watch video
  271. method:"post",
  272. url:"/statistics/api/online-videos",
  273. data:`{"user_id":${userID},"org_id":${orgID},"course_id":${courseID},"activity_id":${actID},"action_type":"view","ts":${Date.now()}}`
  274. })
  275.  
  276. if(now >= need)
  277. {
  278. console.log("速刷已完成");
  279. return;
  280. }
  281.  
  282. const title = document.querySelector("span.title"),
  283. origText = title.innerText
  284. let res;
  285. for(let nowTime=Math.floor(endTime*0.01*Math.max(now-10,0)); nowTime!=endTime;)
  286. {
  287. const dur = endTime-nowTime<120 ? endTime-nowTime : Math.floor(120-Math.random()*66),
  288. newTime = nowTime + dur
  289. res = await requests({ // watch video api
  290. method: "POST",
  291. url: `/api/course/activities-read/${actID}`,
  292. data: `{"start":${nowTime},"end":${newTime}}`,
  293. headers: {"Content-Type": "application/json"},
  294. type: "json"
  295. }).catch(()=>alert("速刷失敗,請聯絡作者"))
  296.  
  297. await requests({ // increase student stat video watching time
  298. method:"post",
  299. url:"/statistics/api/online-videos",
  300. 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}}`
  301. })
  302. await requests({ // increase student stat times for access course
  303. method:"post",
  304. url:"/statistics/api/user-visits",
  305. data:`{"user_id":"${userID}","org_id":${orgID},"course_id":"${courseID}","visit_duration":${dur}}`
  306. })
  307.  
  308. console.log(`${res.response.data.completeness}% ${nowTime}-${newTime} (${endTime})`)
  309. title.innerHTML = `<b>[速刷中] (${res.response.data.completeness}%) ${newTime}/${endTime}s</b> ${origText}`
  310. nowTime = newTime
  311. }
  312. await delay(100)
  313. if(res.response.data.completeness < need) alert("速刷失敗,請聯絡作者")
  314. location.reload()
  315. }
  316.  
  317. const myStat = async () =>
  318. {
  319. const courseID = location.href.match(/(?<=course\/)\d+/)
  320.  
  321. }
  322.  
  323. let lock = false;
  324. waitElementLoad("#ngProgress",1,0,50).then((e)=>{
  325. new MutationObserver(()=>{
  326. if(lock==false && e.style.width=="100%")
  327. {
  328. lock = true
  329. // embedLink()
  330. waitElementLoad("span[ng-bind='ui.duration|formatTime']").then(videoSpeedrun).catch(()=>{})
  331. // if(location.href.match(/course\/\d+/)) myStat()
  332. }
  333. else if (e.style.width=="0%") lock = false;
  334. }).observe(e, {attributes:true})
  335. })
  336. waitElementLoad("[data-category=tronclass-footer]",1,10,200).then((e)=>e.remove())
  337.  
  338.  
  339.