AtCoder Standings Watcher

順位表を定期的に取得して、お気に入りの人の動きを通知します

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