您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Watch standings and notifies
当前为
// ==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(); })();