Last active
September 30, 2025 16:44
-
-
Save loadedsith/1084b0b20b5178f1cae47fdc1301351d to your computer and use it in GitHub Desktop.
sora-utils.user.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Sora Quota Checker | |
| // @namespace https://sora.chatgpt.com/ | |
| // @version 0.2.0 | |
| // @description Check Sora request quota and next available time with a small MenuBar UI | |
| // @author graham.p.heath@gmail.com | |
| // @match https://sora.chatgpt.com/* | |
| // @match *://sora.chatgpt.com/* | |
| // @include https://sora.chatgpt.com/* | |
| // @run-at document-end | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=chatgpt.com | |
| // @grant unsafeWindow | |
| // @grant GM_info | |
| // @grant GM_getValue | |
| // @grant GM.getValue | |
| // @grant GM_setValue | |
| // @grant GM.setValue | |
| // @grant GM_xmlhttpRequest | |
| // @grant GM.xmlHttpRequest | |
| // @require https://gist.githubusercontent.com/loadedsith/0e26f07a2098344a5127f9eebc2fee4c/raw/menuBar.js | |
| // ==/UserScript== | |
| var SoraUtilsBundle = (() => { | |
| // src/quota-utils.js | |
| function extractTaskCreatedAtTimestamps(jsonRoot) { | |
| const results = []; | |
| const stack = [jsonRoot]; | |
| while (stack.length) { | |
| const current = stack.pop(); | |
| if (Array.isArray(current)) { | |
| for (const item of current) stack.push(item); | |
| } else if (current && typeof current === "object") { | |
| if (typeof current.created_at === "string" && typeof current.id === "string" && current.id.startsWith("task_")) { | |
| results.push(current.created_at); | |
| } | |
| for (const value of Object.values(current)) stack.push(value); | |
| } | |
| } | |
| return results; | |
| } | |
| function timestampsToSortedDates(timestamps) { | |
| return timestamps.map((iso) => { | |
| const d = new Date(iso); | |
| return Number.isNaN(d.getTime()) ? null : d; | |
| }).filter((d) => d !== null).sort((a, b) => a.getTime() - b.getTime()); | |
| } | |
| function formatInTimeZone(date, timeZone, locale) { | |
| const resolvedLocale = locale || (typeof navigator !== "undefined" ? navigator.languages && navigator.languages[0] || navigator.language || "en-US" : "en-US"); | |
| const formatter = new Intl.DateTimeFormat(resolvedLocale, { | |
| timeZone, | |
| weekday: "short", | |
| year: "numeric", | |
| month: "short", | |
| day: "2-digit", | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| second: "2-digit", | |
| hour12: true, | |
| timeZoneName: "short" | |
| }); | |
| return formatter.format(date); | |
| } | |
| function computeQuotaSummary(dates, options = {}) { | |
| const limit = typeof options.limit === "number" ? options.limit : 12; | |
| const now = options.now instanceof Date ? options.now : /* @__PURE__ */ new Date(); | |
| const windowMs = 24 * 60 * 60 * 1e3; | |
| const windowStartMs = now.getTime() - windowMs; | |
| const requestsInLast24h = dates.filter((d) => d.getTime() >= windowStartMs).length; | |
| const remaining = Math.max(0, limit - requestsInLast24h); | |
| const canRequestNow = remaining > 0; | |
| let nextAllowedAt = null; | |
| if (!canRequestNow && dates.length >= limit) { | |
| const nthFromEnd = dates[dates.length - limit]; | |
| nextAllowedAt = new Date(nthFromEnd.getTime() + windowMs); | |
| } | |
| return { requestsInLast24h, remaining, canRequestNow, nextAllowedAt }; | |
| } | |
| // userscripts/sora-utils.user.entry.js | |
| (function() { | |
| const defaultTz = Intl.DateTimeFormat().resolvedOptions().timeZone || "America/Chicago"; | |
| let bridgeInstalled = false; | |
| let cooldownUntilMs = 0; | |
| let lastDates = []; | |
| let lastRemaining = 0; | |
| let tokenCaptured = false; | |
| function findJwtLikeStringInObject(obj) { | |
| const jwtRegex = /[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+/; | |
| try { | |
| const str = typeof obj === "string" ? obj : JSON.stringify(obj); | |
| const m = str && str.match(jwtRegex); | |
| return m ? m[0] : null; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function detectTokenFromStorage() { | |
| const candidates = []; | |
| try { | |
| for (let i = 0; i < localStorage.length; i += 1) { | |
| const k = localStorage.key(i); | |
| candidates.push(localStorage.getItem(k)); | |
| } | |
| } catch { | |
| } | |
| try { | |
| for (let i = 0; i < sessionStorage.length; i += 1) { | |
| const k = sessionStorage.key(i); | |
| candidates.push(sessionStorage.getItem(k)); | |
| } | |
| } catch { | |
| } | |
| try { | |
| if (window.__NEXT_DATA__) candidates.push(window.__NEXT_DATA__); | |
| } catch { | |
| } | |
| for (const val of candidates) { | |
| const jwt = findJwtLikeStringInObject(val); | |
| if (jwt) return jwt; | |
| } | |
| return null; | |
| } | |
| function sanitizeJwtToken(input) { | |
| if (!input) return null; | |
| let str = String(input).trim(); | |
| const bearerMatch = str.match(/^Bearer\s+(.+)$/i); | |
| if (bearerMatch) str = bearerMatch[1]; | |
| const ascii = str.replace(/[^\x20-\x7E]/g, ""); | |
| const jwtMatch = ascii.match( | |
| /([A-Za-z0-9_-]{16,})\.([A-Za-z0-9_-]{16,})\.([A-Za-z0-9_-]{10,})/ | |
| ); | |
| return jwtMatch ? jwtMatch[0] : null; | |
| } | |
| async function checkQuota(limit = 12, tz = defaultTz, token) { | |
| const url = `https://sora.chatgpt.com/backend/notif?limit=${encodeURIComponent(limit)}`; | |
| const headers = { | |
| Accept: "application/json, text/plain, */*", | |
| "Cache-Control": "no-cache", | |
| Pragma: "no-cache" | |
| }; | |
| const resolvedToken = sanitizeJwtToken(token) || sanitizeJwtToken(detectTokenFromStorage()); | |
| if (resolvedToken) headers.Authorization = `Bearer ${resolvedToken}`; | |
| const sameOrigin = (() => { | |
| try { | |
| return new URL(url).origin === window.location.origin; | |
| } catch { | |
| return false; | |
| } | |
| })(); | |
| const useGM = !sameOrigin && (typeof GM_xmlhttpRequest === "function" || typeof GM !== "undefined" && GM.xmlHttpRequest); | |
| const data = await new Promise((resolve, reject) => { | |
| if (!useGM) { | |
| pageFetchJsonViaBridge(url, headers).then(resolve).catch(reject); | |
| } else { | |
| const gm = typeof GM_xmlhttpRequest === "function" ? GM_xmlhttpRequest : GM.xmlHttpRequest; | |
| gm({ | |
| method: "GET", | |
| url, | |
| headers, | |
| anonymous: false, | |
| onload: (response) => { | |
| try { | |
| if (response.status < 200 || response.status >= 300) { | |
| reject( | |
| new Error(`Sora notif fetch failed: ${response.status} ${response.statusText}`) | |
| ); | |
| return; | |
| } | |
| resolve(JSON.parse(response.responseText)); | |
| } catch (e) { | |
| reject(e); | |
| } | |
| }, | |
| onerror: (err) => reject(new Error(`Sora notif request error: ${err?.error || "network"}`)), | |
| ontimeout: () => reject(new Error("Sora notif request timeout")), | |
| timeout: 15e3 | |
| }); | |
| } | |
| }); | |
| const timestamps = extractTaskCreatedAtTimestamps(data); | |
| const dates = timestampsToSortedDates(timestamps); | |
| const { remaining, canRequestNow, nextAllowedAt, requestsInLast24h } = computeQuotaSummary( | |
| dates, | |
| { limit } | |
| ); | |
| const now = /* @__PURE__ */ new Date(); | |
| const within24h = dates.filter((d) => now.getTime() - d.getTime() < 24 * 60 * 60 * 1e3); | |
| return { | |
| summary: { | |
| canRequestNow, | |
| remaining, | |
| requestsInLast24h, | |
| nextAllowedAtUtc: nextAllowedAt ? nextAllowedAt.toISOString() : null, | |
| nextAllowedAtLocal: nextAllowedAt ? formatInTimeZone(nextAllowedAt, tz) : null, | |
| totalParsed: dates.length | |
| }, | |
| dates, | |
| within24h | |
| }; | |
| } | |
| const menuBar = new MenuBar({ id: "sora-quota", title: "Sora Quota" }); | |
| menuBar.addSection({ id: "quota", title: "Quota" }); | |
| menuBar.addSection({ id: "actions", title: "Actions" }); | |
| const textId = "quota-summary"; | |
| menuBar.addText({ id: textId, sectionId: "quota", content: 'Click "Check Now"\u2026' }); | |
| menuBar.addButton({ | |
| id: "check-now", | |
| label: "Check Now", | |
| sectionId: "actions", | |
| handler: async () => { | |
| performCheck(menuBar, textId).catch((e) => { | |
| menuBar.addLog(`[Sora] Error: ${e.message}`); | |
| alert("Sora Quota Error: " + e.message); | |
| }); | |
| } | |
| }); | |
| async function performCheck(menuBar2, textId2) { | |
| const nowMs = Date.now(); | |
| if (cooldownUntilMs && nowMs < cooldownUntilMs) return; | |
| cooldownUntilMs = nowMs + 1e4; | |
| setButtonDisabled(true); | |
| const savedToken = await GM.getValue("sora-token", ""); | |
| const result = await checkQuota(12, defaultTz, savedToken || void 0); | |
| const { summary, within24h } = result; | |
| lastDates = within24h; | |
| lastRemaining = summary.remaining; | |
| const snippet = buildSnippet(lastDates, lastRemaining); | |
| requestAnimationFrame(() => { | |
| renderQuotaUI(menuBar2, lastDates, lastRemaining, defaultTz, snippet); | |
| const quotaContainer = menuBar2._menuBar?.querySelector( | |
| "[data-section-id='quota'] .section-content" | |
| ); | |
| if (quotaContainer) quotaContainer.style.flexDirection = "column-reverse"; | |
| const placeholder = quotaContainer?.querySelector(`[data-id='${textId2}']`); | |
| if (placeholder) placeholder.innerHTML = snippet; | |
| }); | |
| } | |
| function setButtonDisabled(disabled) { | |
| const btn = menuBar._menuBar?.querySelector("[data-id='check-now']"); | |
| if (btn) { | |
| btn.disabled = !!disabled; | |
| btn.style.opacity = disabled ? "0.6" : ""; | |
| btn.style.pointerEvents = disabled ? "none" : ""; | |
| } | |
| } | |
| function renderQuotaUI(menu, recentDates, remaining, tz, snippetText) { | |
| const total = 12; | |
| const used = Math.min(recentDates.length, total); | |
| const graph = `${"\u{1F7E5}".repeat(used)}${"\u{1F7E9}".repeat(Math.max(0, total - used))}`.replace(/(.{8})/g, "$1 ").trim(); | |
| const container = menu._menuBar?.querySelector("[data-section-id='quota'] .section-content"); | |
| if (!container) { | |
| menu.addLog("[Sora] quota container not found"); | |
| return; | |
| } | |
| container.style.flexDirection = "column-reverse"; | |
| let host = container.querySelector('[data-id="quota-table"]'); | |
| if (!host) { | |
| host = document.createElement("div"); | |
| host.setAttribute("data-id", "quota-table"); | |
| container.appendChild(host); | |
| } | |
| host.innerHTML = ""; | |
| const graphEl = document.createElement("div"); | |
| graphEl.title = `Used: ${used} of ${total}, available ${remaining}`; | |
| graphEl.textContent = `${graph}`; | |
| graphEl.style.margin = "6px 0"; | |
| host.appendChild(graphEl); | |
| } | |
| function formatDuration(ms) { | |
| const s = Math.max(0, Math.floor(ms / 1e3)); | |
| const h = Math.floor(s / 3600); | |
| const m = Math.floor(s % 3600 / 60); | |
| const sec = s % 60; | |
| if (h > 0) return `${h}h ${m}m ${sec}s`; | |
| if (m > 0) return `${m}m ${sec}s`; | |
| return `${sec}s`; | |
| } | |
| function formatCountdownLocale(ms) { | |
| const locale = typeof navigator !== "undefined" && (navigator.languages && navigator.languages[0] || navigator.language) || "en-US"; | |
| const s = Math.max(0, Math.floor(ms / 1e3)); | |
| const d = new Date(s * 1e3); | |
| return d.toLocaleTimeString(locale, { | |
| timeZone: "UTC", | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| second: "2-digit", | |
| hour12: false | |
| }); | |
| } | |
| function buildSnippet(recentDates, remaining) { | |
| const total = 12; | |
| const now = /* @__PURE__ */ new Date(); | |
| const untils = recentDates.map( | |
| (d) => Math.max(0, 24 * 60 * 60 * 1e3 - (now.getTime() - d.getTime())) | |
| ); | |
| const used = recentDates.length; | |
| const nextMs = used === 0 ? 0 : Math.min(...untils); | |
| const allMs = used === 0 ? 0 : Math.max(...untils); | |
| const monoStyle = "font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;"; | |
| const nextPart = nextMs === 0 && remaining > 0 ? "Next slot available now" : `<span style="${monoStyle}">${formatCountdownLocale(nextMs)} until next slot</span>`; | |
| const allPart = allMs === 0 ? `All ${total} available now` : `<span style="${monoStyle}">${formatCountdownLocale(allMs)} until all ${total} slots</span>`; | |
| const cooldownMs = Math.max(0, cooldownUntilMs - Date.now()); | |
| const cd = cooldownMs > 0 ? `<br/><span style="${monoStyle}">${Math.ceil(cooldownMs / 1e3)}</span> left in cooldown` : ""; | |
| return `${nextPart}<br/>${allPart}${cd}`; | |
| } | |
| function installTokenSniffers(menu) { | |
| const capture = async (maybeAuthHeader) => { | |
| const token = sanitizeJwtToken(maybeAuthHeader); | |
| if (token) { | |
| await GM.setValue("sora-token", token); | |
| debugEnabled && menu?.addLog?.(`Captured token ${maskToken(token)}`); | |
| } else { | |
| if (debugEnabled) { | |
| const preview = String(maybeAuthHeader || "").slice(0, 24); | |
| menu?.addLog?.(`Authorization seen but not JWT; got: "${preview}..."`); | |
| } | |
| } | |
| }; | |
| try { | |
| const originalFetch = window.fetch; | |
| if (typeof originalFetch === "function") { | |
| window.fetch = new Proxy(originalFetch, { | |
| apply(target, thisArg, args) { | |
| try { | |
| const [input, init] = args; | |
| let headers = null; | |
| if (init && init.headers) headers = init.headers; | |
| else if (input && input.headers) headers = input.headers; | |
| let authVal = null; | |
| if (headers) { | |
| if (headers instanceof Headers) { | |
| authVal = headers.get("authorization") || headers.get("Authorization"); | |
| } else if (Array.isArray(headers)) { | |
| const found = headers.find(([k]) => k && k.toLowerCase() === "authorization"); | |
| authVal = found ? found[1] : null; | |
| } else if (typeof headers === "object") { | |
| authVal = headers.authorization || headers.Authorization || null; | |
| } | |
| } | |
| if (authVal) capture(authVal); | |
| } catch { | |
| } | |
| return Reflect.apply(target, thisArg, args); | |
| } | |
| }); | |
| } | |
| } catch { | |
| } | |
| try { | |
| const OriginalXHR = window.XMLHttpRequest; | |
| if (OriginalXHR) { | |
| const Proto = OriginalXHR.prototype; | |
| const originalSetHeader = Proto.setRequestHeader; | |
| Proto.setRequestHeader = function(name, value) { | |
| try { | |
| if (name && String(name).toLowerCase() === "authorization") capture(value); | |
| } catch { | |
| } | |
| return originalSetHeader.apply(this, arguments); | |
| }; | |
| } | |
| } catch { | |
| } | |
| } | |
| function maskToken(t) { | |
| if (!t) return ""; | |
| const s = String(t); | |
| return s.length <= 12 ? s : `${s.slice(0, 6)}...${s.slice(-6)}`; | |
| } | |
| installTokenSniffers(menuBar); | |
| installPageBridge(); | |
| function installPageBridge() { | |
| if (bridgeInstalled) return; | |
| const script = document.createElement("script"); | |
| script.type = "text/javascript"; | |
| script.textContent = `(() => { | |
| if (window.__soraQuotaBridgeInstalled) return; window.__soraQuotaBridgeInstalled = true; | |
| // Lightweight token extraction from page context | |
| const sendToken = (val) => { | |
| try { | |
| if (!val) return; | |
| const s = String(val); | |
| const bearer = s.match(/^Bearers+(.+)$/i); | |
| const raw = bearer ? bearer[1] : s; | |
| const m = raw.match(/([A-Za-z0-9_-]{16,}).([A-Za-z0-9_-]{16,}).([A-Za-z0-9_-]{10,})/); | |
| if (m) window.postMessage({ type: 'sora-utils:capture-token', token: m[0] }, '*'); | |
| } catch {} | |
| }; | |
| // Patch fetch | |
| try { | |
| const _fetch = window.fetch; | |
| if (typeof _fetch === 'function') { | |
| window.fetch = new Proxy(_fetch, { | |
| apply(target, thisArg, args) { | |
| try { | |
| const [input, init] = args; | |
| let headers = null; | |
| if (init && init.headers) headers = init.headers; | |
| else if (input && input.headers) headers = input.headers; | |
| let auth = null; | |
| if (headers) { | |
| if (headers instanceof Headers) { | |
| auth = headers.get('authorization') || headers.get('Authorization'); | |
| } else if (Array.isArray(headers)) { | |
| const f = headers.find(([k]) => k && k.toLowerCase() === 'authorization'); | |
| auth = f ? f[1] : null; | |
| } else if (typeof headers === 'object') { | |
| auth = headers.authorization || headers.Authorization || null; | |
| } | |
| } | |
| if (auth) sendToken(auth); | |
| } catch {} | |
| return Reflect.apply(target, thisArg, args); | |
| } | |
| }); | |
| } | |
| } catch {} | |
| // Patch Headers.set | |
| try { | |
| const _set = Headers.prototype.set; | |
| Headers.prototype.set = function(name, value) { | |
| try { if (String(name).toLowerCase() === 'authorization') sendToken(value); } catch {} | |
| return _set.apply(this, arguments); | |
| }; | |
| } catch {} | |
| // Patch XHR | |
| try { | |
| const _xhrSet = XMLHttpRequest.prototype.setRequestHeader; | |
| XMLHttpRequest.prototype.setRequestHeader = function(name, value) { | |
| try { if (String(name).toLowerCase() === 'authorization') sendToken(value); } catch {} | |
| return _xhrSet.apply(this, arguments); | |
| }; | |
| } catch {} | |
| window.addEventListener('message', async (event) => { | |
| try { | |
| const data = event.data; | |
| if (!data || data.type !== 'sora-utils:fetch') return; | |
| const { id, url, headers } = data; | |
| let resp, text, ok=false, status=0, statusText=''; | |
| try { | |
| resp = await fetch(url, { method: 'GET', credentials: 'include', headers, referrer: location.href, referrerPolicy: 'strict-origin-when-cross-origin' }); | |
| text = await resp.text(); | |
| ok = resp.ok; status = resp.status; statusText = resp.statusText; | |
| window.postMessage({ type: 'sora-utils:response', id, ok, status, statusText, body: text }, '*'); | |
| } catch (err) { | |
| window.postMessage({ type: 'sora-utils:response', id, error: String(err) }, '*'); | |
| } | |
| } catch (e) {} | |
| }); | |
| })();`; | |
| document.documentElement.appendChild(script); | |
| script.remove(); | |
| bridgeInstalled = true; | |
| } | |
| function pageFetchJsonViaBridge(url, headers) { | |
| return new Promise((resolve, reject) => { | |
| const id = `sf-${Math.random().toString(36).slice(2)}`; | |
| const handler = (event) => { | |
| const data = event.data; | |
| if (!data || data.type !== "sora-utils:response" || data.id !== id) return; | |
| window.removeEventListener("message", handler); | |
| if (data.error) return reject(new Error(data.error)); | |
| if (!data.ok) { | |
| const snippet = (data.body || "").slice(0, 200); | |
| return reject( | |
| new Error(`Sora notif fetch failed: ${data.status} ${data.statusText} :: ${snippet}`) | |
| ); | |
| } | |
| try { | |
| resolve(JSON.parse(data.body)); | |
| } catch (e) { | |
| reject(e); | |
| } | |
| }; | |
| window.addEventListener("message", handler); | |
| window.postMessage({ type: "sora-utils:fetch", id, url, headers }, "*"); | |
| }); | |
| } | |
| const tokenListener = async (event) => { | |
| if (tokenCaptured) return; | |
| const d = event.data; | |
| if (!d || d.type !== "sora-utils:capture-token" || !d.token) return; | |
| const token = sanitizeJwtToken(d.token); | |
| if (token) { | |
| tokenCaptured = true; | |
| await GM.setValue("sora-token", token); | |
| performCheck(menuBar, textId).catch(() => { | |
| }); | |
| window.removeEventListener("message", tokenListener); | |
| } | |
| }; | |
| window.addEventListener("message", tokenListener); | |
| setInterval(() => { | |
| if (lastDates && lastDates.length) { | |
| const snippet = buildSnippet(lastDates, lastRemaining); | |
| const quotaContainer = menuBar._menuBar?.querySelector( | |
| "[data-section-id='quota'] .section-content" | |
| ); | |
| const placeholder = quotaContainer?.querySelector(`[data-id='${textId}']`); | |
| if (placeholder) placeholder.innerHTML = snippet; | |
| } | |
| const now = Date.now(); | |
| if (cooldownUntilMs && now >= cooldownUntilMs) { | |
| cooldownUntilMs = 0; | |
| setButtonDisabled(false); | |
| } | |
| }, 1e3); | |
| })(); | |
| })(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Sora Quota Checker | |
| // @namespace https://sora.chatgpt.com/ | |
| // @version 0.2.0 | |
| // @description Check Sora request quota and next available time with a small MenuBar UI | |
| // @author graham.p.heath@gmail.com | |
| // @match https://sora.chatgpt.com/* | |
| // @match *://sora.chatgpt.com/* | |
| // @include https://sora.chatgpt.com/* | |
| // @run-at document-end | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=chatgpt.com | |
| // @grant unsafeWindow | |
| // @grant GM_info | |
| // @grant GM_getValue | |
| // @grant GM.getValue | |
| // @grant GM_setValue | |
| // @grant GM.setValue | |
| // @grant GM_xmlhttpRequest | |
| // @grant GM.xmlHttpRequest | |
| // @require https://gist.githubusercontent.com/loadedsith/0e26f07a2098344a5127f9eebc2fee4c/raw/menuBar.js | |
| // ==/UserScript== | |
| var SoraUtilsBundle = (() => { | |
| // src/quota-utils.js | |
| function extractTaskCreatedAtTimestamps(jsonRoot) { | |
| const results = []; | |
| const stack = [jsonRoot]; | |
| while (stack.length) { | |
| const current = stack.pop(); | |
| if (Array.isArray(current)) { | |
| for (const item of current) stack.push(item); | |
| } else if (current && typeof current === "object") { | |
| if (typeof current.created_at === "string" && typeof current.id === "string" && current.id.startsWith("task_")) { | |
| results.push(current.created_at); | |
| } | |
| for (const value of Object.values(current)) stack.push(value); | |
| } | |
| } | |
| return results; | |
| } | |
| function timestampsToSortedDates(timestamps) { | |
| return timestamps.map((iso) => { | |
| const d = new Date(iso); | |
| return Number.isNaN(d.getTime()) ? null : d; | |
| }).filter((d) => d !== null).sort((a, b) => a.getTime() - b.getTime()); | |
| } | |
| function formatInTimeZone(date, timeZone, locale) { | |
| const resolvedLocale = locale || (typeof navigator !== "undefined" ? navigator.languages && navigator.languages[0] || navigator.language || "en-US" : "en-US"); | |
| const formatter = new Intl.DateTimeFormat(resolvedLocale, { | |
| timeZone, | |
| weekday: "short", | |
| year: "numeric", | |
| month: "short", | |
| day: "2-digit", | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| second: "2-digit", | |
| hour12: true, | |
| timeZoneName: "short" | |
| }); | |
| return formatter.format(date); | |
| } | |
| function computeQuotaSummary(dates, options = {}) { | |
| const limit = typeof options.limit === "number" ? options.limit : 12; | |
| const now = options.now instanceof Date ? options.now : /* @__PURE__ */ new Date(); | |
| const windowMs = 24 * 60 * 60 * 1e3; | |
| const windowStartMs = now.getTime() - windowMs; | |
| const requestsInLast24h = dates.filter((d) => d.getTime() > now.getTime() - windowMs - 1).length; | |
| const remaining = Math.max(0, limit - requestsInLast24h); | |
| const canRequestNow = remaining > 0; | |
| let nextAllowedAt = null; | |
| if (!canRequestNow && dates.length >= limit) { | |
| const nthFromEnd = dates[dates.length - limit]; | |
| nextAllowedAt = new Date(nthFromEnd.getTime() + windowMs); | |
| } | |
| return { requestsInLast24h, remaining, canRequestNow, nextAllowedAt }; | |
| } | |
| // userscripts/sora-utils.user.entry.js | |
| (function() { | |
| const defaultTz = Intl.DateTimeFormat().resolvedOptions().timeZone || "America/Chicago"; | |
| let bridgeInstalled = false; | |
| let cooldownUntilMs = 0; | |
| let lastDates = []; | |
| let lastRemaining = 0; | |
| let tokenCaptured = false; | |
| function findJwtLikeStringInObject(obj) { | |
| const jwtRegex = /[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+/; | |
| try { | |
| const str = typeof obj === "string" ? obj : JSON.stringify(obj); | |
| const m = str && str.match(jwtRegex); | |
| return m ? m[0] : null; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function detectTokenFromStorage() { | |
| const candidates = []; | |
| try { | |
| for (let i = 0; i < localStorage.length; i += 1) { | |
| const k = localStorage.key(i); | |
| candidates.push(localStorage.getItem(k)); | |
| } | |
| } catch { | |
| } | |
| try { | |
| for (let i = 0; i < sessionStorage.length; i += 1) { | |
| const k = sessionStorage.key(i); | |
| candidates.push(sessionStorage.getItem(k)); | |
| } | |
| } catch { | |
| } | |
| try { | |
| if (window.__NEXT_DATA__) candidates.push(window.__NEXT_DATA__); | |
| } catch { | |
| } | |
| for (const val of candidates) { | |
| const jwt = findJwtLikeStringInObject(val); | |
| if (jwt) return jwt; | |
| } | |
| return null; | |
| } | |
| function sanitizeJwtToken(input) { | |
| if (!input) return null; | |
| let str = String(input).trim(); | |
| const bearerMatch = str.match(/^Bearer\s+(.+)$/i); | |
| if (bearerMatch) str = bearerMatch[1]; | |
| const ascii = str.replace(/[^\x20-\x7E]/g, ""); | |
| const jwtMatch = ascii.match( | |
| /([A-Za-z0-9_-]{16,})\.([A-Za-z0-9_-]{16,})\.([A-Za-z0-9_-]{10,})/ | |
| ); | |
| return jwtMatch ? jwtMatch[0] : null; | |
| } | |
| async function checkQuota(limit = 12, tz = defaultTz, token) { | |
| const FETCH_LIMIT = 15; | |
| const url = `https://sora.chatgpt.com/backend/notif?limit=${encodeURIComponent(FETCH_LIMIT)}`; | |
| const headers = { | |
| Accept: "application/json, text/plain, */*", | |
| "Cache-Control": "no-cache", | |
| Pragma: "no-cache" | |
| }; | |
| const resolvedToken = sanitizeJwtToken(token) || sanitizeJwtToken(detectTokenFromStorage()); | |
| if (resolvedToken) headers.Authorization = `Bearer ${resolvedToken}`; | |
| const sameOrigin = (() => { | |
| try { | |
| return new URL(url).origin === window.location.origin; | |
| } catch { | |
| return false; | |
| } | |
| })(); | |
| const useGM = !sameOrigin && (typeof GM_xmlhttpRequest === "function" || typeof GM !== "undefined" && GM.xmlHttpRequest); | |
| const data = await new Promise((resolve, reject) => { | |
| if (!useGM) { | |
| pageFetchJsonViaBridge(url, headers).then(resolve).catch(reject); | |
| } else { | |
| const gm = typeof GM_xmlhttpRequest === "function" ? GM_xmlhttpRequest : GM.xmlHttpRequest; | |
| gm({ | |
| method: "GET", | |
| url, | |
| headers, | |
| anonymous: false, | |
| onload: (response) => { | |
| try { | |
| if (response.status < 200 || response.status >= 300) { | |
| reject( | |
| new Error(`Sora notif fetch failed: ${response.status} ${response.statusText}`) | |
| ); | |
| return; | |
| } | |
| resolve(JSON.parse(response.responseText)); | |
| } catch (e) { | |
| reject(e); | |
| } | |
| }, | |
| onerror: (err) => reject(new Error(`Sora notif request error: ${err?.error || "network"}`)), | |
| ontimeout: () => reject(new Error("Sora notif request timeout")), | |
| timeout: 15e3 | |
| }); | |
| } | |
| }); | |
| const timestamps = extractTaskCreatedAtTimestamps(data); | |
| const dates = timestampsToSortedDates(timestamps); | |
| const { remaining, canRequestNow, nextAllowedAt, requestsInLast24h } = computeQuotaSummary( | |
| dates, | |
| { limit } | |
| ); | |
| const now = /* @__PURE__ */ new Date(); | |
| const within24h = dates.filter((d) => now.getTime() - d.getTime() <= 24 * 60 * 60 * 1e3); | |
| return { | |
| summary: { | |
| canRequestNow, | |
| remaining, | |
| requestsInLast24h, | |
| nextAllowedAtUtc: nextAllowedAt ? nextAllowedAt.toISOString() : null, | |
| nextAllowedAtLocal: nextAllowedAt ? formatInTimeZone(nextAllowedAt, tz) : null, | |
| totalParsed: dates.length | |
| }, | |
| dates, | |
| within24h | |
| }; | |
| } | |
| const menuBar = new MenuBar({ id: "sora-quota", title: "Sora Quota" }); | |
| initAnalytics(); | |
| track("sora_quota_page_load"); | |
| menuBar.addSection({ id: "quota", title: "Quota" }); | |
| menuBar.addSection({ id: "actions", title: "Actions" }); | |
| const textId = "quota-summary"; | |
| menuBar.addText({ id: textId, sectionId: "quota", content: 'Click "Check Now"\u2026' }); | |
| menuBar.addButton({ | |
| id: "check-now", | |
| label: "Check Now", | |
| sectionId: "actions", | |
| handler: async () => { | |
| track("sora_quota_check_click"); | |
| performCheck(menuBar, textId).catch((e) => { | |
| menuBar.addLog(`[Sora] Error: ${e.message}`); | |
| alert("Sora Quota Error: " + e.message); | |
| }); | |
| } | |
| }); | |
| async function performCheck(menuBar2, textId2) { | |
| const nowMs = Date.now(); | |
| if (cooldownUntilMs && nowMs < cooldownUntilMs) return; | |
| cooldownUntilMs = nowMs + 1e4; | |
| setButtonDisabled(true); | |
| const savedToken = await GM.getValue("sora-token", ""); | |
| const result = await checkQuota(12, defaultTz, savedToken || void 0); | |
| const { summary, within24h } = result; | |
| lastDates = within24h; | |
| lastRemaining = summary.remaining; | |
| const snippet = buildSnippet(lastDates, lastRemaining); | |
| requestAnimationFrame(() => { | |
| renderQuotaUI(menuBar2, lastDates, lastRemaining, defaultTz, snippet); | |
| const quotaContainer = menuBar2._menuBar?.querySelector( | |
| "[data-section-id='quota'] .section-content" | |
| ); | |
| if (quotaContainer) quotaContainer.style.flexDirection = "column-reverse"; | |
| const placeholder = quotaContainer?.querySelector(`[data-id='${textId2}']`); | |
| if (placeholder) placeholder.innerHTML = snippet; | |
| }); | |
| } | |
| function setButtonDisabled(disabled) { | |
| const btn = menuBar._menuBar?.querySelector("[data-id='check-now']"); | |
| if (btn) { | |
| btn.disabled = !!disabled; | |
| btn.style.opacity = disabled ? "0.6" : ""; | |
| btn.style.pointerEvents = disabled ? "none" : ""; | |
| } | |
| } | |
| function renderQuotaUI(menu, recentDates, remaining, tz, snippetText) { | |
| const total = 12; | |
| const used = Math.min(recentDates.length, total); | |
| const graph = `${"\u{1F7E5}".repeat(used)}${"\u{1F7E9}".repeat(Math.max(0, total - used))}`.replace(/(.{8})/g, "$1 ").trim(); | |
| const container = menu._menuBar?.querySelector("[data-section-id='quota'] .section-content"); | |
| if (!container) { | |
| menu.addLog("[Sora] quota container not found"); | |
| return; | |
| } | |
| container.style.flexDirection = "column-reverse"; | |
| let host = container.querySelector('[data-id="quota-table"]'); | |
| if (!host) { | |
| host = document.createElement("div"); | |
| host.setAttribute("data-id", "quota-table"); | |
| container.appendChild(host); | |
| } | |
| host.innerHTML = ""; | |
| const graphEl = document.createElement("div"); | |
| graphEl.title = `Used: ${used} of ${total}, available ${remaining}`; | |
| graphEl.textContent = `${graph}`; | |
| graphEl.style.margin = "6px 0"; | |
| host.appendChild(graphEl); | |
| } | |
| function formatDuration(ms) { | |
| const s = Math.max(0, Math.floor(ms / 1e3)); | |
| const h = Math.floor(s / 3600); | |
| const m = Math.floor(s % 3600 / 60); | |
| const sec = s % 60; | |
| if (h > 0) return `${h}h ${m}m ${sec}s`; | |
| if (m > 0) return `${m}m ${sec}s`; | |
| return `${sec}s`; | |
| } | |
| function formatCountdownLocale(ms) { | |
| const locale = typeof navigator !== "undefined" && (navigator.languages && navigator.languages[0] || navigator.language) || "en-US"; | |
| const s = Math.max(0, Math.floor(ms / 1e3)); | |
| const d = new Date(s * 1e3); | |
| return d.toLocaleTimeString(locale, { | |
| timeZone: "UTC", | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| second: "2-digit", | |
| hour12: false | |
| }); | |
| } | |
| function buildSnippet(recentDates, remaining) { | |
| const total = 12; | |
| const now = /* @__PURE__ */ new Date(); | |
| const untils = recentDates.map( | |
| (d) => Math.max(0, 24 * 60 * 60 * 1e3 - (now.getTime() - d.getTime())) | |
| ); | |
| const used = recentDates.length; | |
| const nextMs = used === 0 ? 0 : Math.min(...untils); | |
| const allMs = used === 0 ? 0 : Math.max(...untils); | |
| const monoStyle = "font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;"; | |
| const nextPart = nextMs === 0 && remaining > 0 ? "Next slot available now" : `<span style="${monoStyle}">${formatCountdownLocale(nextMs)} until next slot</span>`; | |
| const allPart = allMs === 0 ? `All ${total} available now` : `<span style="${monoStyle}">${formatCountdownLocale(allMs)} until all ${total} slots</span>`; | |
| const cooldownMs = Math.max(0, cooldownUntilMs - Date.now()); | |
| const cd = cooldownMs > 0 ? `<br/><span style="${monoStyle}">${Math.ceil(cooldownMs / 1e3)}</span> left in cooldown` : ""; | |
| return `${nextPart}<br/>${allPart}${cd}`; | |
| } | |
| function installTokenSniffers(menu) { | |
| const capture = async (maybeAuthHeader) => { | |
| const token = sanitizeJwtToken(maybeAuthHeader); | |
| if (token) { | |
| await GM.setValue("sora-token", token); | |
| debugEnabled && menu?.addLog?.(`Captured token ${maskToken(token)}`); | |
| } else { | |
| if (debugEnabled) { | |
| const preview = String(maybeAuthHeader || "").slice(0, 24); | |
| menu?.addLog?.(`Authorization seen but not JWT; got: "${preview}..."`); | |
| } | |
| } | |
| }; | |
| try { | |
| const originalFetch = window.fetch; | |
| if (typeof originalFetch === "function") { | |
| window.fetch = new Proxy(originalFetch, { | |
| apply(target, thisArg, args) { | |
| try { | |
| const [input, init] = args; | |
| let headers = null; | |
| if (init && init.headers) headers = init.headers; | |
| else if (input && input.headers) headers = input.headers; | |
| let authVal = null; | |
| if (headers) { | |
| if (headers instanceof Headers) { | |
| authVal = headers.get("authorization") || headers.get("Authorization"); | |
| } else if (Array.isArray(headers)) { | |
| const found = headers.find(([k]) => k && k.toLowerCase() === "authorization"); | |
| authVal = found ? found[1] : null; | |
| } else if (typeof headers === "object") { | |
| authVal = headers.authorization || headers.Authorization || null; | |
| } | |
| } | |
| if (authVal) capture(authVal); | |
| } catch { | |
| } | |
| return Reflect.apply(target, thisArg, args); | |
| } | |
| }); | |
| } | |
| } catch { | |
| } | |
| try { | |
| const OriginalXHR = window.XMLHttpRequest; | |
| if (OriginalXHR) { | |
| const Proto = OriginalXHR.prototype; | |
| const originalSetHeader = Proto.setRequestHeader; | |
| Proto.setRequestHeader = function(name, value) { | |
| try { | |
| if (name && String(name).toLowerCase() === "authorization") capture(value); | |
| } catch { | |
| } | |
| return originalSetHeader.apply(this, arguments); | |
| }; | |
| } | |
| } catch { | |
| } | |
| } | |
| function maskToken(t) { | |
| if (!t) return ""; | |
| const s = String(t); | |
| return s.length <= 12 ? s : `${s.slice(0, 6)}...${s.slice(-6)}`; | |
| } | |
| installTokenSniffers(menuBar); | |
| installPageBridge(); | |
| function installPageBridge() { | |
| if (bridgeInstalled) return; | |
| const script = document.createElement("script"); | |
| script.type = "text/javascript"; | |
| script.textContent = `(() => { | |
| if (window.__soraQuotaBridgeInstalled) return; window.__soraQuotaBridgeInstalled = true; | |
| // Lightweight token extraction from page context | |
| const sendToken = (val) => { | |
| try { | |
| if (!val) return; | |
| const s = String(val); | |
| const bearer = s.match(/^Bearers+(.+)$/i); | |
| const raw = bearer ? bearer[1] : s; | |
| const m = raw.match(/([A-Za-z0-9_-]{16,}).([A-Za-z0-9_-]{16,}).([A-Za-z0-9_-]{10,})/); | |
| if (m) window.postMessage({ type: 'sora-utils:capture-token', token: m[0] }, '*'); | |
| } catch {} | |
| }; | |
| // Patch fetch | |
| try { | |
| const _fetch = window.fetch; | |
| if (typeof _fetch === 'function') { | |
| window.fetch = new Proxy(_fetch, { | |
| apply(target, thisArg, args) { | |
| try { | |
| const [input, init] = args; | |
| let headers = null; | |
| if (init && init.headers) headers = init.headers; | |
| else if (input && input.headers) headers = input.headers; | |
| let auth = null; | |
| if (headers) { | |
| if (headers instanceof Headers) { | |
| auth = headers.get('authorization') || headers.get('Authorization'); | |
| } else if (Array.isArray(headers)) { | |
| const f = headers.find(([k]) => k && k.toLowerCase() === 'authorization'); | |
| auth = f ? f[1] : null; | |
| } else if (typeof headers === 'object') { | |
| auth = headers.authorization || headers.Authorization || null; | |
| } | |
| } | |
| if (auth) sendToken(auth); | |
| } catch {} | |
| return Reflect.apply(target, thisArg, args); | |
| } | |
| }); | |
| } | |
| } catch {} | |
| // Patch Headers.set | |
| try { | |
| const _set = Headers.prototype.set; | |
| Headers.prototype.set = function(name, value) { | |
| try { if (String(name).toLowerCase() === 'authorization') sendToken(value); } catch {} | |
| return _set.apply(this, arguments); | |
| }; | |
| } catch {} | |
| // Patch XHR | |
| try { | |
| const _xhrSet = XMLHttpRequest.prototype.setRequestHeader; | |
| XMLHttpRequest.prototype.setRequestHeader = function(name, value) { | |
| try { if (String(name).toLowerCase() === 'authorization') sendToken(value); } catch {} | |
| return _xhrSet.apply(this, arguments); | |
| }; | |
| } catch {} | |
| window.addEventListener('message', async (event) => { | |
| try { | |
| const data = event.data; | |
| if (!data || data.type !== 'sora-utils:fetch') return; | |
| const { id, url, headers } = data; | |
| let resp, text, ok=false, status=0, statusText=''; | |
| try { | |
| resp = await fetch(url, { method: 'GET', credentials: 'include', headers, referrer: location.href, referrerPolicy: 'strict-origin-when-cross-origin' }); | |
| text = await resp.text(); | |
| ok = resp.ok; status = resp.status; statusText = resp.statusText; | |
| window.postMessage({ type: 'sora-utils:response', id, ok, status, statusText, body: text }, '*'); | |
| } catch (err) { | |
| window.postMessage({ type: 'sora-utils:response', id, error: String(err) }, '*'); | |
| } | |
| } catch (e) {} | |
| }); | |
| })();`; | |
| document.documentElement.appendChild(script); | |
| script.remove(); | |
| bridgeInstalled = true; | |
| } | |
| function pageFetchJsonViaBridge(url, headers) { | |
| return new Promise((resolve, reject) => { | |
| const id = `sf-${Math.random().toString(36).slice(2)}`; | |
| const handler = (event) => { | |
| const data = event.data; | |
| if (!data || data.type !== "sora-utils:response" || data.id !== id) return; | |
| window.removeEventListener("message", handler); | |
| if (data.error) return reject(new Error(data.error)); | |
| if (!data.ok) { | |
| const snippet = (data.body || "").slice(0, 200); | |
| return reject( | |
| new Error(`Sora notif fetch failed: ${data.status} ${data.statusText} :: ${snippet}`) | |
| ); | |
| } | |
| try { | |
| resolve(JSON.parse(data.body)); | |
| } catch (e) { | |
| reject(e); | |
| } | |
| }; | |
| window.addEventListener("message", handler); | |
| window.postMessage({ type: "sora-utils:fetch", id, url, headers }, "*"); | |
| }); | |
| } | |
| const tokenListener = async (event) => { | |
| if (tokenCaptured) return; | |
| const d = event.data; | |
| if (!d || d.type !== "sora-utils:capture-token" || !d.token) return; | |
| const token = sanitizeJwtToken(d.token); | |
| if (token) { | |
| tokenCaptured = true; | |
| await GM.setValue("sora-token", token); | |
| performCheck(menuBar, textId).catch(() => { | |
| }); | |
| window.removeEventListener("message", tokenListener); | |
| } | |
| }; | |
| window.addEventListener("message", tokenListener); | |
| setInterval(() => { | |
| if (lastDates && lastDates.length) { | |
| const snippet = buildSnippet(lastDates, lastRemaining); | |
| const quotaContainer = menuBar._menuBar?.querySelector( | |
| "[data-section-id='quota'] .section-content" | |
| ); | |
| const placeholder = quotaContainer?.querySelector(`[data-id='${textId}']`); | |
| if (placeholder) placeholder.innerHTML = snippet; | |
| } | |
| const now = Date.now(); | |
| if (cooldownUntilMs && now >= cooldownUntilMs) { | |
| cooldownUntilMs = 0; | |
| setButtonDisabled(false); | |
| } | |
| }, 1e3); | |
| function initAnalytics() { | |
| const projectId = "170840"; | |
| const apiKey = "phc_2vR6m5PQyhS3ExKjYqoOlkII3zowFHhniOY8GcpTkU2"; | |
| if (!projectId || !apiKey) return; | |
| window.__phq = []; | |
| window.posthog = { | |
| capture: (event, props) => { | |
| try { | |
| window.__phq.push({ event, properties: props || {} }); | |
| const payload = { | |
| api_key: apiKey, | |
| event, | |
| properties: { | |
| distinct_id: "sora-utils-userscript", | |
| project_id: projectId, | |
| ...props | |
| } | |
| }; | |
| const blob = new Blob([JSON.stringify(payload)], { type: "application/json" }); | |
| if (navigator.sendBeacon) { | |
| navigator.sendBeacon("https://app.posthog.com/capture/", blob); | |
| } else { | |
| fetch("https://app.posthog.com/capture/", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload), | |
| keepalive: true | |
| }).catch(() => { | |
| }); | |
| } | |
| } catch { | |
| } | |
| } | |
| }; | |
| } | |
| function track(event, props) { | |
| try { | |
| if (!window.posthog || typeof window.posthog.capture !== "function") return; | |
| if (typeof navigator !== "undefined" && navigator.doNotTrack === "1") return; | |
| window.posthog.capture(event, { | |
| ts: (/* @__PURE__ */ new Date()).toISOString(), | |
| version: typeof GM_info !== "undefined" && GM_info?.script?.version || "dev" | |
| }); | |
| } catch { | |
| } | |
| } | |
| })(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment