Skip to content

Instantly share code, notes, and snippets.

@magurofly
Created July 5, 2021 07:47
Show Gist options
  • Save magurofly/244b647453c04163c34b045cb3d94fa9 to your computer and use it in GitHub Desktop.
Save magurofly/244b647453c04163c34b045cb3d94fa9 to your computer and use it in GitHub Desktop.
AtCoder Standings Watcher
// ==UserScript==
// @name AtCoder Standings Watcher
// @namespace https://atcoder.jp/
// @version 0.2.7
// @description Watch standings and notifies
// @author magurofly
// @match https://atcoder.jp/contests/*
// @icon https://www.google.com/s2/favicons?domain=atcoder.jp
// @grant GM_notification
// @grant unsafeWindow
// ==/UserScript==
(function() {
'use strict';
// 各種設定
const INTERVAL = 30e3; // 更新間隔(ミリ秒単位)
const NOTIFICATION_TIMEOUT = 15e3; // 通知の表示時間(ミリ秒単位)
const NOTIFICATION_TEMPLATES = {
penalty: ({user, task}) => `${user.id} さんが ${task.assignment} - ${task.name} でペナルティを出しました`,
accepted: ({user, task, score}) => `${user.id} さんが ${task.assignment} - ${task.name} で ${score} 点を獲得し、 ${user.rank} 位になりました`,
};
// 定数
const watchingContest = unsafeWindow.contestScreenName;
const standingsAPI = `/contests/${watchingContest}/standings/json`;
const channel = new BroadcastChannel("atcoder-standings-watcher");
const startTime = +unsafeWindow.startTime; // コンテスト開始時刻
const openTime = Date.now();
// 状態
let lastUpdate = 0; // 最後に更新した時刻
const watchingUsers = {};
// 関数
async function initialize() {
console.dir(() => {});
if (unsafeWindow.getServerTime().isAfter(unsafeWindow.endTime)) { // コンテストが終了している
console.info("AtCoder Standings Watcher: contest has ended");
return;
}
channel.onmessage = ({data}) => {
console.log("AtCoder Standings Watcher: receive: ", data);
switch (data.type) {
case "update":
if (data.contest == watchingContest) lastUpdate = Math.max(lastUpdate, data.time);
break;
case "task":
watchingUsers[data.userId].taskResults[data.taskId] = data.result;
break
}
};
const favs = await getFavs();
for (const fav of favs) {
watchingUsers[fav] = {
id: fav,
rank: 0,
taskResults: {},
count: 0,
penalty: 0,
score: 0,
};
}
await sleep(INTERVAL);
await update(false).catch(error => console.error(error));
await sleep(INTERVAL);
for (;; await sleep(INTERVAL)) {
const now = Date.now();
if (now - lastUpdate <= INTERVAL) {
// INTERVAL 以内に更新していた(衝突が発生した)場合
await sleep(INTERVAL * Math.random());
continue;
}
lastUpdate = now;
channel.postMessage({ type: "update", contest: watchingContest, time: lastUpdate });
update().catch(error => console.error(error));
}
}
async function update(notifyChanges = true) {
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];
user.rank = standing.Rank;
user.count = standing.TotalResult.Count;
user.penalty = standing.TotalResult.Penalty;
user.score = standing.TotalResult.Score;
for (const task in standing.TaskResults) {
const result = user[task] || (user[task] = { count: 0, penalty: 0, score: 0 });
const Result = standing.TaskResults[task];
const isNew = (startTime + task.Elapsed * 1e-6 < openTime);
if (Result.Penalty > result.penalty) {
result.penalty = Result.Penalty;
if (isNew && notifyChanges) notify({ user, task: tasks[task], type: "penalty" });
}
if (Result.Score > result.score) {
result.score = Result.Score;
if (isNew && notifyChanges) notify({ user, task: tasks[task], type: "accepted", score: result.score / 100 });
}
}
}
}
function notify(notification) {
console.log("AtCoder Standings Watcher: notification: ", notification);
GM_notification({
text: NOTIFICATION_TEMPLATES[notification.type](notification),
timeout: NOTIFICATION_TIMEOUT,
});
if (notification.user && notification.task && notification.user.taskResults) {
channel.postMessage({ type: "task", userId: notification.user.id, taskId: notification.task.id, result: notification.user.taskResults[notification.task.id] });
}
}
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();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment