// ==UserScript== // @name Gym Order Injector // @namespace https://github.com/jungheil/goInjector // @license Mulan PSL v2 // @copyright 2025 Jungheil // @version 1.2.1 // @description Inject order into Gym Booking System // @icon https://www.sysu.edu.cn/favicon.ico // @author Jungheil // @homepage https://github.com/jungheil/goInjector // @match *://gym.sysu.edu.cn/* // @grant none // @run-at document-start // @downloadURL https://raw.githubusercontent.com/jungheil/goInjector/main/goInjector.user.js // @updateURL https://raw.githubusercontent.com/jungheil/goInjector/main/goInjector.meta.js // @supportURL https://github.com/jungheil/goInjector // ==/UserScript== // Copyright (c) 2025 Jungheil // Gym Order Injector is licensed under Mulan PSL v2. // You can use this software according to the terms and conditions of the Mulan PSL v2. // You may obtain a copy of Mulan PSL v2 at: // http://license.coscl.org.cn/MulanPSL2 // THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, // EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, // MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. // See the Mulan PSL v2 for more details. (function () { "use strict"; let initializationAttempts = 0; const maxAttempts = 3; async function tryInitialize() { try { console.log( `goInjector 开始初始化 (尝试 ${initializationAttempts + 1 }/${maxAttempts})` ); // Original XHR open override const xhrOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function () { let venueConfig = JSON.parse(localStorage.getItem("venueConfig")); const xhr = this; let url = new URL(arguments[1]); if ( (url.pathname == "/api/BookingRequestVenue" && venueConfig?.isInitiator) || (url.pathname == "/api/BookingRequestVenue/Participants" && !venueConfig?.isInitiator) ) { const getter = Object.getOwnPropertyDescriptor( XMLHttpRequest.prototype, "responseText" ).get; Object.defineProperty(xhr, "responseText", { get: () => { let result = getter.call(xhr); const bookingInfo = localStorage.getItem("BookingInfo"); if (bookingInfo) { let ret = JSON.parse(result); ret.push(JSON.parse(bookingInfo)); return JSON.stringify(ret); } return result; }, }); } return xhrOpen.apply(xhr, arguments); }; const venueType = await getVenueType(); await generateBookingInfo(); initializeUI(venueType); console.log("goInjector 初始化完成"); return true; } catch (error) { console.error( `goInjector 初始化失败 (尝试 ${initializationAttempts + 1 }/${maxAttempts}):`, error ); return false; } } async function initializeWithRetry() { console.log("goInjector 准备初始化..."); console.log("当前页面状态:", document.readyState); while (initializationAttempts < maxAttempts) { const success = await tryInitialize(); if (success) { return; } initializationAttempts++; if (initializationAttempts < maxAttempts) { console.log( `goInjector 将在 1 秒后重试初始化 (${initializationAttempts + 1 }/${maxAttempts})...` ); await new Promise((resolve) => setTimeout(resolve, 1000)); } } console.error(`goInjector 初始化失败,已达到最大重试次数 (${maxAttempts})`); } function startInitialization() { if (localStorage.getItem("scientia-session-authorization")) { // 立即尝试初始化 initializeWithRetry(); } else { console.log("未登录,goInjector 放弃初始化"); } } // 使用标志位防止重复初始化 let hasInitialized = false; function handleInitialization() { if (!hasInitialized) { console.log("页面状态:", document.readyState); hasInitialized = true; startInitialization(); } } // 监听 readystatechange 事件 document.addEventListener("readystatechange", function handler(e) { if (document.readyState !== "loading") { document.removeEventListener("readystatechange", handler); handleInitialization(); } }); // 如果页面已经加载完成,立即开始初始化 if (document.readyState !== "loading") { handleInitialization(); } function requestVenueType() { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("GET", "https://gym.sysu.edu.cn/api/venuetype/all", true); const access_token = JSON.parse( localStorage.getItem("scientia-session-authorization") ).access_token; xhr.setRequestHeader("Authorization", "Bearer " + access_token); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.responseText); } else { reject(new Error(`get venue type failed, status: ${xhr.status}`)); } } }; xhr.onerror = () => { reject(new Error("get venue type failed")); }; xhr.send(); }); } async function getVenueType() { let vt = localStorage.getItem("venueType"); if (!vt) { vt = await requestVenueType(); localStorage.setItem("venueType", vt); } return JSON.parse(vt); } function requestVenue(venueTypeId) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open( "GET", "https://gym.sysu.edu.cn/api/venue/type/" + venueTypeId, true ); const access_token = JSON.parse( localStorage.getItem("scientia-session-authorization") ).access_token; xhr.setRequestHeader("Authorization", "Bearer " + access_token); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.responseText); } else { reject(new Error(`get venue failed, status: ${xhr.status}`)); } } }; xhr.onerror = () => { reject(new Error("get venue failed")); }; xhr.send(); }); } function requestMe() { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("GET", "https://gym.sysu.edu.cn/api/Credit/Me", true); const access_token = JSON.parse( localStorage.getItem("scientia-session-authorization") ).access_token; xhr.setRequestHeader("Authorization", "Bearer " + access_token); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.responseText); } else { reject(new Error(`get me failed, status: ${xhr.status}`)); } } }; xhr.onerror = () => { reject(new Error("get me failed")); }; xhr.send(); }); } async function getMe() { var Me = localStorage.getItem("Me"); if (!Me) { Me = await requestMe(); localStorage.setItem("Me", Me); } return JSON.parse(Me); } function requestCreditFee(VenueTypeId) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open( "GET", "https://gym.sysu.edu.cn/api/venuetype/" + VenueTypeId + "/feetemplates", true ); const access_token = JSON.parse( localStorage.getItem("scientia-session-authorization") ).access_token; xhr.setRequestHeader("Authorization", "Bearer " + access_token); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200) { const result = JSON.parse(xhr.responseText).find( (item) => item.UserRole === "学生" ).CreditFee; resolve(result); } else { reject(new Error(`get credit fee failed, status: ${xhr.status}`)); } } }; xhr.onerror = () => { reject(new Error("get credit fee failed")); }; xhr.send(); }); } function getRandomTime(start, end) { const startDate = new Date(start); const endDate = new Date(end); const randomDate = new Date( startDate.getTime() + Math.random() * (endDate.getTime() - startDate.getTime()) ); return randomDate; } async function generateBookingInfo() { const venueConfig = JSON.parse(localStorage.getItem("venueConfig")); const Me = await getMe(); if (venueConfig && venueConfig.VenueTypeId && venueConfig.VenueId) { try { const VenueTypeInfo = (await getVenueType()).find( (item) => item.Identity === venueConfig.VenueTypeId ); const VenueInfo = JSON.parse( await requestVenue(venueConfig.VenueTypeId) ).find((item) => item.Identity === venueConfig.VenueId); let startDateTime, endDateTime; if (venueConfig.timeSlot && venueConfig.bookingDate) { let timeSlot = JSON.parse(venueConfig.timeSlot); startDateTime = new Date( venueConfig.bookingDate + "T" + timeSlot.startTime.split("T")[1] ); endDateTime = new Date( venueConfig.bookingDate + "T" + timeSlot.endTime.split("T")[1] ); } else { let now = new Date(); const timeSlots = VenueInfo.Schedules[0].TimeSlots; const currentTime = now.getHours() + now.getMinutes() / 60; const bookingTime = currentTime + 0.25; let targetSlot; for (let slot of timeSlots) { const slotStartTime = parseInt(slot.End.split(":")[0]) + parseInt(slot.End.split(":")[1]) / 60; if (slotStartTime >= bookingTime) { targetSlot = slot; break; } } if (!targetSlot) { targetSlot = timeSlots[0]; now.setDate(now.getDate() + 1); } if (bookingTime < parseInt(timeSlots[0].Start.split(":")[0])) { targetSlot = timeSlots[0]; } startDateTime = new Date( now.getFullYear(), now.getMonth(), now.getDate(), parseInt(targetSlot.Start.split(":")[0]), parseInt(targetSlot.Start.split(":")[1]) ); endDateTime = new Date( now.getFullYear(), now.getMonth(), now.getDate(), parseInt(targetSlot.End.split(":")[0]), parseInt(targetSlot.End.split(":")[1]) ); } const CreateAt = getRandomTime( startDateTime.getTime() - 2 * 24 * 60 * 60 * 1000, startDateTime.getTime() ); const UpdateAt = getRandomTime(CreateAt, endDateTime); if (venueConfig.isInitiator) { let BookingInfo = { Identity: venueConfig.Identity || crypto.randomUUID(), Name: venueConfig.Name || Me.Name, BookingId: venueConfig.bookingId || "#RB-" + Array(10) .fill() .map( () => "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[ Math.floor(Math.random() * 36) ] ) .join(""), UserId: venueConfig.UserId || Me.UserId, VenueTypeId: venueConfig.VenueTypeId, VenueId: venueConfig.VenueId, VenueName: VenueInfo.Name, StartDateTime: startDateTime, EndDateTime: endDateTime, Participants: venueConfig.participant .filter((p) => p.Name && p.HostKey) .map((p) => ({ UserId: p.UserId || Array(Math.floor(4 + Math.random() * 4)) .fill() .map( () => "abcdefghijklmnopqrstuvwxyz"[ Math.floor(Math.random() * 26) ] ) .join("") + Math.floor(Math.random() * 999).toString(), Name: p.Name + " (" + p.HostKey + ")", HostKey: p.HostKey, Status: "Accepted", })), Status: "Accepted", Description: VenueTypeInfo.Name, CreatedAt: CreateAt, UpdatedAt: UpdateAt, ActionedBy: venueConfig.ActionedBy || (venueConfig.participant && venueConfig.participant[ Math.floor(Math.random() * venueConfig.participant.length) ]?.UserId) || venueConfig.UserId || Me.UserId, Charge: await requestCreditFee(venueConfig.VenueTypeId), IsCash: false, }; localStorage.setItem("BookingInfo", JSON.stringify(BookingInfo)); } else if (!venueConfig.isInitiator && venueConfig.Name) { let BookingInfo = { Identity: venueConfig.Identity || crypto.randomUUID(), Name: venueConfig.Name, BookingId: venueConfig.BookingId || "#RB-" + Array(10) .fill() .map( () => "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[ Math.floor(Math.random() * 36) ] ) .join(""), UserId: venueConfig.UserId || Array(Math.floor(4 + Math.random() * 4)) .fill() .map( () => "abcdefghijklmnopqrstuvwxyz"[Math.floor(Math.random() * 26)] ) .join("") + Math.floor(Math.random() * 999).toString(), VenueTypeId: venueConfig.VenueTypeId, VenueId: venueConfig.VenueId, VenueName: VenueInfo.Name, StartDateTime: startDateTime, EndDateTime: endDateTime, Participants: venueConfig.participant.filter((p) => p.Name && p.HostKey) .length === 0 ? [ { UserId: Me.UserId, Name: Me.Name + " (" + Me.HostKey + ")", HostKey: Me.HostKey, Status: "Accepted", }, ] : venueConfig.participant .filter((p) => p.Name && p.HostKey) .map((p) => ({ UserId: p.UserId || Array(Math.floor(4 + Math.random() * 4)) .fill() .map( () => "abcdefghijklmnopqrstuvwxyz"[ Math.floor(Math.random() * 26) ] ) .join("") + Math.floor(Math.random() * 999).toString(), Name: p.Name + " (" + p.HostKey + ")", HostKey: p.HostKey, Status: "Accepted", })), Status: "Accepted", Description: VenueTypeInfo.Name, CreatedAt: CreateAt, UpdatedAt: UpdateAt, ActionedBy: venueConfig.ActionedBy || (venueConfig.participant && venueConfig.participant[ Math.floor(Math.random() * venueConfig.participant.length) ]?.UserId) || Me.UserId || venueConfig.UserId, Charge: await requestCreditFee(venueConfig.VenueTypeId), IsCash: false, }; localStorage.setItem("BookingInfo", JSON.stringify(BookingInfo)); } else { localStorage.setItem("BookingInfo", ""); } } catch (error) { console.error("生成预约信息失败:", error); localStorage.setItem("BookingInfo", ""); } } else { localStorage.setItem("BookingInfo", ""); } } function initializeUI(venueType) { let participants = []; const style = document.createElement("style"); style.textContent = ` .config-button { position: fixed; left: 20px; top: 20px; z-index: 9999; padding: 10px 20px; background: rgba(255, 255, 255, 0); color: rgba(255, 255, 255, 0.02); border: none; border-radius: 4px; cursor: pointer; transition: opacity 0.3s; } .config-button:hover { opacity: 0.8; } .config-dialog { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 10000; max-height: 80vh; overflow-y: auto; min-width: 400px; width: 90%; max-width: 600px; } .config-dialog::-webkit-scrollbar { width: 8px; } .config-dialog::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; } .config-dialog::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; } .config-dialog::-webkit-scrollbar-thumb:hover { background: #555; } .config-dialog input { display: block; margin: 10px 0; padding: 5px; width: 300px; } .input-group { margin-bottom: 12px; } .input-label { display: block; font-size: 12px; color: #063; margin-bottom: 2px; } .config-dialog select, .config-dialog input { width: 100%; padding: 6px 12px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; } .button-group { display: flex; justify-content: flex-end; gap: 10px; } .save-btn { padding: 8px 20px; background: #063; color: white; border: none; border-radius: 4px; cursor: pointer; } .cancel-btn { padding: 8px 20px; background: #f5f5f5; color: #666; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; } h3 { color: #063; margin-bottom: 16px; } .participants-section { margin-top: 12px; } .participant-item { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } .participant-item input { flex: 1; min-width: 0; } .remove-participant { background: none; border: none; color: #ff4444; font-size: 18px; cursor: pointer; padding: 5px; line-height: 1; transition: color 0.2s; } .remove-participant:hover { color: #ff0000; } .add-participant { background: #063; color: white; border: none; border-radius: 4px; cursor: pointer; padding: 8px 16px; margin-bottom: 16px; transition: all 0.3s ease; } .add-participant:disabled { background: #cccccc; cursor: not-allowed; opacity: 0.7; } .collapsible { margin-top: 12px; } .collapse-header { display: flex; align-items: center; cursor: pointer; user-select: none; color: #063; font-size: 14px; margin-bottom: 12px; } .collapse-header:hover { opacity: 0.8; } .collapse-icon { width: 16px; height: 16px; margin-right: 8px; transition: transform 0.3s; color: #063; } .collapse-icon.expanded { transform: rotate(90deg); } .collapse-content { display: none; } .collapse-content.expanded { display: block; } `; document.head.appendChild(style); const dialog = document.createElement("div"); dialog.className = "config-dialog"; dialog.innerHTML = `