AtCoder Standings Watcher

Watch standings and notifies

2021-07-03 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

  1. // ==UserScript==
  2. // @name AtCoder Standings Watcher
  3. // @namespace https://atcoder.jp/
  4. // @version 0.2.1
  5. // @description Watch standings and notifies
  6. // @author magurofly
  7. // @match https://atcoder.jp/contests/*
  8. // @icon https://www.google.com/s2/favicons?domain=atcoder.jp
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_notification
  12. // @grant unsafeWindow
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // 各種設定
  19. const INTERVAL = 10.0e3; // 更新間隔(ミリ秒単位)
  20. const NOTIFICATION_TIMEOUT = 10;
  21. const NOTIFICATION_TEMPLATES = {
  22. penalty: ({user, task}) => `${user.id} さんが ${task.assignment} - ${task.name} でペナルティを出しました`,
  23. accepted: ({user, task, score}) => `${user.id} さんが ${task.assignment} - ${task.name} ${score} 点を獲得しました`,
  24. };
  25.  
  26. // 定数
  27. const watchingContest = unsafeWindow.contestScreenName;
  28. const standingsAPI = `/contests/${watchingContest}/standings/json`;
  29. const channel = new BroadcastChannel("atcoder-standings-watcher");
  30.  
  31. // 状態
  32. let lastUpdate = 0; // 最後に更新した時刻
  33. const watchingUsers = {};
  34.  
  35. // 関数
  36. async function initialize() {
  37. channel.onmessage = (notification) => {
  38. switch (notification.type) {
  39. case "update":
  40. if (notification.contest == watchingContest) lastUpdate = Math.max(lastUpdate, notification.time);
  41. break;
  42. case "task":
  43. watchingUsers[notification.userId].taskResults[notification.taskId] = notification.result;
  44. break
  45. }
  46. };
  47.  
  48. setTimeout(() => {
  49. update(true);
  50. setInterval(() => {
  51. const now = Date.now();
  52. if (now - lastUpdate <= INTERVAL) return; // INTERVAL 以内に更新していた場合、今回は見送る
  53. lastUpdate = now;
  54. channel.postMessage({ type: "update", contest: watchingContest, time: lastUpdate });
  55. update();
  56. }, INTERVAL);
  57. }, INTERVAL);
  58.  
  59. const favs = await getFavs();
  60. for (const fav of favs) {
  61. watchingUsers[fav] = {
  62. id: fav,
  63. rank: 0,
  64. taskResults: {},
  65. };
  66. }
  67. }
  68.  
  69. async function update(ignore = false) {
  70. console.info("AtCoder Standings Watcher: update");
  71. const data = await getStandingsData();
  72. const tasks = {};
  73. for (const {TaskScreenName, Assignment, TaskName} of data.TaskInfo) {
  74. tasks[TaskScreenName] = { id: TaskScreenName, assignment: Assignment, name: TaskName };
  75. }
  76. for (const standing of data.StandingsData) {
  77. const userId = standing.UserScreenName;
  78. if (!(userId in watchingUsers)) continue;
  79. const user = watchingUsers[userId];
  80.  
  81. if (standing.Rank != user.rank) {
  82. user.rank = standing.Rank;
  83. }
  84. for (const task in standing.TaskResults) {
  85. const result = user[task] || (user[task] = { count: 0, penalty: 0, score: 0 });
  86. const Result = standing.TaskResults[task];
  87. if (Result.Penalty > result.penalty) {
  88. result.penalty = Result.Penalty;
  89. if (!ignore) notify({ user, task: tasks[task], type: "penalty" });
  90. }
  91. if (Result.Score > result.score) {
  92. result.score = Result.Score;
  93. if (!ignore) notify({ user, task: tasks[task], type: "accepted", score: result.score / 100 });
  94. }
  95. }
  96. }
  97. }
  98.  
  99. function notify(notification) {
  100. if (notification.user && notification.task) {
  101. channel.postMessage({ type: "task", userId: notification.user.id, taskId: notification.task.id, result: notification.user.taskResults[notification.task.id] });
  102. }
  103. GM_notification({
  104. text: NOTIFICATION_TEMPLATES[notification.type](notification),
  105. timeout: NOTIFICATION_TIMEOUT,
  106. }, null);
  107. }
  108.  
  109. async function getFavs() {
  110. while (!unsafeWindow.favSet) {
  111. unsafeWindow.reloadFavs();
  112. await sleep(100);
  113. }
  114. return unsafeWindow.favSet;
  115. }
  116.  
  117. async function getStandingsData() {
  118. return await fetch(standingsAPI).then(response => response.json());
  119. }
  120.  
  121. const sleep = (ms) => new Promise(done => setInterval(done, ms));
  122.  
  123. // 初期化
  124. initialize();
  125. })();