Watch standings and notifies
As of
- // ==UserScript==
- // @name AtCoder Standings Watcher
- // @namespace https://atcoder.jp/
- // @version 0.2.1
- // @description Watch standings and notifies
- // @author magurofly
- // @match https://atcoder.jp/contests/*
- // @icon https://www.google.com/s2/favicons?domain=atcoder.jp
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_notification
- // @grant unsafeWindow
- // ==/UserScript==
- (function() {
- 'use strict';
- // 各種設定
- const INTERVAL = 10.0e3; // 更新間隔(ミリ秒単位)
- const NOTIFICATION_TIMEOUT = 10;
- const NOTIFICATION_TEMPLATES = {
- penalty: ({user, task}) => `${user.id} さんが ${task.assignment} - ${task.name} でペナルティを出しました`,
- accepted: ({user, task, score}) => `${user.id} さんが ${task.assignment} - ${task.name} で ${score} 点を獲得しました`,
- };
- // 定数
- const watchingContest = unsafeWindow.contestScreenName;
- const standingsAPI = `/contests/${watchingContest}/standings/json`;
- const channel = new BroadcastChannel("atcoder-standings-watcher");
- // 状態
- let lastUpdate = 0; // 最後に更新した時刻
- const watchingUsers = {};
- // 関数
- async function initialize() {
- channel.onmessage = (notification) => {
- switch (notification.type) {
- case "update":
- if (notification.contest == watchingContest) lastUpdate = Math.max(lastUpdate, notification.time);
- break;
- case "task":
- watchingUsers[notification.userId].taskResults[notification.taskId] = notification.result;
- break
- }
- };
- setTimeout(() => {
- update(true);
- setInterval(() => {
- const now = Date.now();
- if (now - lastUpdate <= INTERVAL) return; // INTERVAL 以内に更新していた場合、今回は見送る
- lastUpdate = now;
- channel.postMessage({ type: "update", contest: watchingContest, time: lastUpdate });
- update();
- }, INTERVAL);
- }, INTERVAL);
- const favs = await getFavs();
- for (const fav of favs) {
- watchingUsers[fav] = {
- id: fav,
- rank: 0,
- taskResults: {},
- };
- }
- }
- async function update(ignore = false) {
- console.info("AtCoder Standings Watcher: update");
- const data = await getStandingsData();
- const tasks = {};
- for (const {TaskScreenName, Assignment, TaskName} of data.TaskInfo) {
- tasks[TaskScreenName] = { id: TaskScreenName, assignment: Assignment, name: TaskName };
- }
- for (const standing of data.StandingsData) {
- const userId = standing.UserScreenName;
- if (!(userId in watchingUsers)) continue;
- const user = watchingUsers[userId];
- if (standing.Rank != user.rank) {
- user.rank = standing.Rank;
- }
- for (const task in standing.TaskResults) {
- const result = user[task] || (user[task] = { count: 0, penalty: 0, score: 0 });
- const Result = standing.TaskResults[task];
- if (Result.Penalty > result.penalty) {
- result.penalty = Result.Penalty;
- if (!ignore) notify({ user, task: tasks[task], type: "penalty" });
- }
- if (Result.Score > result.score) {
- result.score = Result.Score;
- if (!ignore) notify({ user, task: tasks[task], type: "accepted", score: result.score / 100 });
- }
- }
- }
- }
- function notify(notification) {
- if (notification.user && notification.task) {
- channel.postMessage({ type: "task", userId: notification.user.id, taskId: notification.task.id, result: notification.user.taskResults[notification.task.id] });
- }
- GM_notification({
- text: NOTIFICATION_TEMPLATES[notification.type](notification),
- timeout: NOTIFICATION_TIMEOUT,
- }, null);
- }
- async function getFavs() {
- while (!unsafeWindow.favSet) {
- unsafeWindow.reloadFavs();
- await sleep(100);
- }
- return unsafeWindow.favSet;
- }
- async function getStandingsData() {
- return await fetch(standingsAPI).then(response => response.json());
- }
- const sleep = (ms) => new Promise(done => setInterval(done, ms));
- // 初期化
- initialize();
- })();