Watch standings and notifies
当前为
// ==UserScript==
// @name AtCoder Standings Watcher
// @namespace https://atcoder.jp/
// @version 0.2
// @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
}
};
setInterval(() => {
const now = Date.now();
if (now - lastUpdate <= INTERVAL) return; // INTERVAL 以内に更新していた場合、今回は見送る
lastUpdate = now;
channel.postMessage({ type: "update", contest: watchingContest, time: lastUpdate });
update();
}, INTERVAL);
const favs = await getFavs();
for (const fav of favs) {
watchingUsers[fav] = {
id: fav,
rank: 0,
taskResults: {},
};
}
await update(true);
}
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();
})();