// ==UserScript== // @name GitHub Commit 定时监控 // @namespace GitHub Commit 定时监控 // @version 0.1.0 // @description 通过脚本猫定时请求GitHub REST API以监控GitHub仓库Commit更新情况 // @author DreamNya // @grant GM_setValue // @grant GM_getValue // @grant CAT_userConfig // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_openInTab // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgd2lkdGg9IjEyOCIgaGVpZ2h0PSIxMjgiPgogICA8Y2lyY2xlIGN4PSI4IiBjeT0iOCIgZmlsbD0iI2ZmZmZmZiIgcj0iOCIvPgogIDxwYXRoIGQ9Im04LDBjNC40MiwwIDgsMy41OCA4LDhhOC4wMSw4LjAxIDAgMCAxIC01LjQ1LDcuNTljLTAuNCwwLjA4IC0wLjU1LC0wLjE3IC0wLjU1LC0wLjM4YzAsLTAuMjcgMC4wMSwtMS4xMyAwLjAxLC0yLjJjMCwtMC43NSAtMC4yNSwtMS4yMyAtMC41NCwtMS40OGMxLjc4LC0wLjIgMy42NSwtMC44OCAzLjY1LC0zLjk1YzAsLTAuODggLTAuMzEsLTEuNTkgLTAuODIsLTIuMTVjMC4wOCwtMC4yIDAuMzYsLTEuMDIgLTAuMDgsLTIuMTJjMCwwIC0wLjY3LC0wLjIyIC0yLjIsMC44MmMtMC42NCwtMC4xOCAtMS4zMiwtMC4yNyAtMiwtMC4yN2MtMC42OCwwIC0xLjM2LDAuMDkgLTIsMC4yN2MtMS41MywtMS4wMyAtMi4yLC0wLjgyIC0yLjIsLTAuODJjLTAuNDQsMS4xIC0wLjE2LDEuOTIgLTAuMDgsMi4xMmMtMC41MSwwLjU2IC0wLjgyLDEuMjggLTAuODIsMi4xNWMwLDMuMDYgMS44NiwzLjc1IDMuNjQsMy45NWMtMC4yMywwLjIgLTAuNDQsMC41NSAtMC41MSwxLjA3Yy0wLjQ2LDAuMjEgLTEuNjEsMC41NSAtMi4zMywtMC42NmMtMC4xNSwtMC4yNCAtMC42LC0wLjgzIC0xLjIzLC0wLjgyYy0wLjY3LDAuMDEgLTAuMjcsMC4zOCAwLjAxLDAuNTNjMC4zNCwwLjE5IDAuNzMsMC45IDAuODIsMS4xM2MwLjE2LDAuNDUgMC42OCwxLjMxIDIuNjksMC45NGMwLDAuNjcgMC4wMSwxLjMgMC4wMSwxLjQ5YzAsMC4yMSAtMC4xNSwwLjQ1IC0wLjU1LDAuMzhhOCw4IDAgMCAxIC01LjQ3LC03LjU5YzAsLTQuNDIgMy41OCwtOCA4LC04eiIvPgo8L3N2Zz4= // @license MIT // @crontab 0 */8 * * * // @connect api.github.com // ==/UserScript== /* ==UserConfig== 全局设置: Timeout: title: 请求超时时间 description: 默认10000ms,最小1000ms,最大60000ms type: number default: 10000 min: 1000 max: 60000 unit: ms NotifyNoUpdate: title: 无更新是否通知 default: '通知' type: select values: ['通知','不通知'] NotifyTimeout: title: 通知持续时间 description: 默认5000ms,最小1000ms type: number default: 5000 min: 1000 unit: ms NotifyMessage: title: 通知内容 default: '通知更新内容' type: select values: ['通知更新内容','仅通知更新情况'] NotifyClick: title: 有新commit点击通知跳转对应commit default: '跳转' type: select values: ['跳转','不跳转'] Authorization: title: Authorization身份验证(填写可访问更多终结点并获得更高的速率限制) description: 留空则忽略,格式:Token/Bearer+空格+令牌内容 type: text --- 监控仓库JSON: Enable: title: 是否启用JSON配置(启用后忽略所有非JSON配置) default: '禁用' type: select values: ['启用','禁用'] JSON: title: JSON(须为标准JSON) description: JSON规则详见脚本页 type: text --- 监控仓库1: Enable: title: 是否启用监控仓库1配置(优先级低于JSON) default: '禁用' type: select values: ['启用','禁用'] owner: title: owner description: 仓库所有者,必填,留空则忽略本设置 type: text repo: title: repo description: 仓库名称,必填,留空则忽略本设置 type: text branch: title: branch description: 仓库分支,选填,不填则监控整个仓库push,填入则监控具体分支 type: text --- 监控仓库2: Enable: title: 是否启用监控仓库2配置(优先级低于JSON) default: '禁用' type: select values: ['启用','禁用'] owner: title: owner description: 仓库所有者,必填,留空则忽略本设置 type: text repo: title: repo description: 仓库名称,必填,留空则忽略本设置 type: text branch: title: branch description: 仓库分支,选填,不填则监控整个仓库push,填入则监控具体分支 type: text --- 监控仓库3: Enable: title: 是否启用监控仓库3配置(优先级低于JSON) default: '禁用' type: select values: ['启用','禁用'] owner: title: owner description: 仓库所有者,必填,留空则忽略本设置 type: text repo: title: repo description: 仓库名称,必填,留空则忽略本设置 type: text branch: title: branch description: 仓库分支,选填,不填则监控整个仓库push,填入则监控具体分支 type: text ==/UserConfig== */ /* 脚本存储规则: - UserConfig Key 用户配置 - 全局设置 - /监控仓库([1-3]|JSON)$/ - lastSHA Key 监控仓库最后监控Commit SHA - lastSHA_${UserConfig Key} */ /* 监控仓库JSON解析规则: - 须为可被JSON.parse解析的标准JSON文本 - 深度1的Key为自定义监控仓库别名,不能以/监控仓库([1-3]|JSON)$/为仓库别名 - 深度2的Key仅解析owner\repo\branch(owner\repo任意为空则忽略,branch可为空)忽略其余Key JSON示例: { "scriptcat-vscode": { "owner": "scriptscat", "repo": "scriptcat-vscode" }, "scriptscat": { "owner": "scriptscat", "repo": "scriptcat", "branch": "develop/beta" } } */ //读取全局设置 const globalSetting = {}; globalSetting.icon = GM_info.script.icon; //自己画的图标……以后再说 globalSetting.Timeout = GM_getValue('全局设置.Timeout', 10000) * 1; globalSetting.NotifyNoUpdate = GM_getValue('全局设置.NotifyNoUpdate', '通知') == '通知'; globalSetting.NotifyTimeout = GM_getValue('全局设置.NotifyTimeout', 5000) * 1; globalSetting.NotifyMessage = GM_getValue('全局设置.NotifyMessage', '通知更新内容') == '通知更新内容'; globalSetting.NotifyClick = GM_getValue('全局设置.NotifyClick', '跳转') == '跳转'; globalSetting.Authorization = GM_getValue('全局设置.Authorization', null); //脚本猫不同脚本相同@namespace共享GM_Value,可能存在令牌被窃取隐患? //主函数 return new Promise(async (resolve) => { const XHRTask = {}; const JSONSetting = {}; JSONSetting.error = ''; JSONSetting.Enable = GM_getValue('监控仓库JSON.Enable', '禁用'); if (JSONSetting.Enable == '启用') { try { const json = JSON.parse(GM_getValue('监控仓库JSON.JSON')) Object.keys(json).forEach((key) => { if (/监控仓库([1-3]|JSON)$/.test(key)) throw new Error('JSON不能以/监控仓库([1-3]|JSON)$/为key') const { owner, repo, branch } = json[key] if (!owner && !repo) return XHRTask[key] = { owner, repo, branch, lastSHA: GM_getValue('lastSHA_' + key) } }) } catch (err) { JSONSetting.Enable = '禁用' JSONSetting.error = err console.log('JSON err', err) } } if (JSONSetting.Enable == '禁用') { ['监控仓库1', '监控仓库2', '监控仓库3'].forEach((key) => { const Enable = GM_getValue(key + '.Enable', '禁用') if (Enable != '启用') return const owner = GM_getValue(key + '.owner') const repo = GM_getValue(key + '.repo') const branch = GM_getValue(key + '.branch') if (!owner && !repo) return XHRTask[key] = { owner, repo, branch, lastSHA: GM_getValue('lastSHA_' + key) } }) } if (Object.keys(XHRTask).length == 0 || JSONSetting.error) { const errResult = JSONSetting.error ? 'JSON配置错误' + JSONSetting.error : '监控仓库配置错误' GM_notification({ text: errResult, title: "Github commit 定时监控", image: globalSetting.icon }) resolve((CAT_userConfig(), errResult)) } else { console.log(XHRTask) } const XHRResult = [] for (const task in XHRTask) { const { owner, repo, branch, lastSHA } = XHRTask[task] const url = `https://api.github.com/repos/${owner}/${repo}/${branch ? 'branches' : 'commits'}` const taskUrl = `${owner}/${repo}${branch ? '/tree/' + branch : ''}` const response = (await GMxhr(url).catch(e => (console.log(e), e.message))).response if (typeof response == 'string') { XHRResult.push(`${taskUrl}\n请求失败 ${response}`) continue } const commit = branch ? response.find(i => i.name == branch)?.commit : response[0] if (!commit) { XHRResult.push(`${taskUrl}\n请求失败 无此branch`) continue } const { sha: SHA, url: commitUrl } = commit if (SHA != lastSHA) { //branch方式不会直接返回commitTitle,需要额外请求一次具体branch const commitTitle = !globalSetting.NotifyMessage ? null : (branch ? await GMxhr(commit.url).then(r => r.response).catch(e => (console.log(e), "commitTitle请求失败\n" + e.message)) : commit ).commit.message const message = `${taskUrl}${taskUrl.length > 30 ? ' ' : '\n'}${lastSHA ? '发现新' : '初始化'}commit` const githubUrl = `https://github.com/${owner}/${repo}/commit/${SHA}` //待定,通知点击后跳转到具体commit XHRResult.push({ message, githubUrl, commitTitle }) GM_setValue('lastSHA_' + task, SHA) } else { XHRResult.push(`${taskUrl}\n无新commit`) } } console.log(XHRResult) //GM_notification队列 async to sync for (const result of XHRResult) { //无更新不通知 if (!globalSetting.NotifyNoUpdate && result.includes?.('无新commit')) continue await GMnotify({ text: (globalSetting.NotifyMessage && result.commitTitle) || ' ', title: result.message || result, image: globalSetting.icon, onclick: globalSetting.NotifyClick && result.githubUrl ? () => GM_openInTab(result.githubUrl, { active: true }) : null, timeout: globalSetting.NotifyTimeout, }) } //脚本运行完毕 resolve(XHRResult.map((result) => result.message || result).join('\n')) }); //全局函数 ↓ //GM_xmlhttpRequest async to sync function GMxhr(url) { // https://docs.github.com/zh/rest/overview/authenticating-to-the-rest-api // 你可以对 REST API 进行身份验证,以访问更多终结点并获得更高的速率限制。 const headers = globalSetting.Authorization ? { "Authorization": globalSetting.Authorization, "X-GitHub-Api-Version": "2022-11-28" } : null return new Promise((resolve, reject) => { GM_xmlhttpRequest({ url, method: 'GET', ...headers, responseType: 'json', timeout: globalSetting.Timeout, ontimeout: (xhr) => reject({ message: 'Timeout', xhr }), onerror: (xhr) => reject({ message: 'Error', xhr }), onload: (xhr) => { console.log(xhr) if (xhr.status != 200) reject({ message: xhr.response.message, xhr }) resolve(xhr) } }) }) } //GM_notification队列 async to sync function GMnotify(config) { return new Promise((resolve) => { GM_notification({ ...config, ondone: () => setTimeout(() => resolve(), 500) //上一个通知结束后间隔500ms显示下一个通知 }) }) }