// ==UserScript== // @name GitHub PR Upload Link Logger // @namespace http://tampermonkey.net/ // @version 1.0 // @description GitHub Pull Request ページで動画・ファイルのアップロード完了後に挿入されるリンクを console.log に出力する // @author @SimplyRin // @match https://github.com/* // @icon https://github.githubassets.com/favicons/favicon.svg // @grant GM_xmlhttpRequest // @updateURL https://raw.githubusercontent.com/SimplyRin/github-pr-approved-viewer/main/src/github-pr-upload-link-logger.user.js // @downloadURL https://raw.githubusercontent.com/SimplyRin/github-pr-approved-viewer/main/src/github-pr-upload-link-logger.user.js // ==/UserScript== // MIT License // Copyright (c) 2026 SimplyRin // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. (function () { 'use strict'; // アップロード完了後に挿入される user-attachments リンクの正規表現 const ATTACHMENT_URL_REGEX = /https:\/\/github\.com\/user-attachments\/assets\/[a-f0-9-]+/g; // 監視中のテキストエリアとその前回の値を保持する WeakMap const previousValues = new WeakMap(); // textarea の value プロパティのネイティブ descriptor(フック回避用) const nativeDescriptor = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value'); function getNativeValue(textarea) { return nativeDescriptor.get.call(textarea); } function setNativeValue(textarea, value) { nativeDescriptor.set.call(textarea, value); } /** * GM_xmlhttpRequest で HEAD リクエストを送り Content-Type を取得する */ function getContentType(url) { console.log('[Upload Link Logger] [DEBUG] GET リクエスト開始:', url); return new Promise((resolve) => { try { GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'Accept': '*/*', }, onload: function (response) { console.log('[Upload Link Logger] [DEBUG] レスポンス受信 status=' + response.status); console.log('[Upload Link Logger] [DEBUG] 最終URL (リダイレクト先):', response.finalUrl); console.log('[Upload Link Logger] [DEBUG] レスポンスヘッダー:', response.responseHeaders); const match = response.responseHeaders.match(/^content-type:\s*(.+)$/mi); const contentType = match ? match[1].trim().split(';')[0].trim() : null; console.log('[Upload Link Logger] [DEBUG] Content-Type:', contentType); resolve(contentType); }, onerror: function (error) { console.error('[Upload Link Logger] [DEBUG] リクエスト失敗 (onerror):', error); console.error('[Upload Link Logger] [DEBUG] error.finalUrl:', error.finalUrl); console.error('[Upload Link Logger] [DEBUG] error.responseHeaders:', error.responseHeaders); console.error('[Upload Link Logger] [DEBUG] error.responseText (先頭500文字):', (error.responseText || '').substring(0, 500)); resolve(null); }, ontimeout: function () { console.error('[Upload Link Logger] [DEBUG] リクエストタイムアウト'); resolve(null); }, timeout: 15000, }); } catch (e) { console.error('[Upload Link Logger] [DEBUG] GM_xmlhttpRequest 呼び出し例外:', e); resolve(null); } }); } /** * テキスト内で URL が HTML 属性(引用符)の中に入っていない(裸の)状態かを判定する */ function isBareLinkInText(text, url) { let pos = 0; while ((pos = text.indexOf(url, pos)) !== -1) { const charBefore = pos > 0 ? text[pos - 1] : ''; const charAfter = pos + url.length < text.length ? text[pos + url.length] : ''; if (charBefore !== '"' && charBefore !== "'" && charAfter !== '"' && charAfter !== "'") { return true; } pos += url.length; } return false; } /** * リンクの Content-Type が video/* なら