GC - Restock Logger

Logs restock events from Grundos Cafe to discord and virtupets.net.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         GC - Restock Logger
// @namespace    https://greasyfork.org/en/users/1278031-crystalflame
// @version      1.0.1
// @description  Logs restock events from Grundos Cafe to discord and virtupets.net.
// @author       CrystalFlame
// @license      MIT
// @match        *://*.grundos.cafe/buyitem/*
// @match        *://*.grundos.cafe/help/*
// @grant        GM.getValue
// @grant        GM.setValue
// @icon         https://www.google.com/s2/favicons?sz=64&domain=grundos.cafe
// @require      https://update.greasyfork.org/scripts/514423/1554918/GC%20-%20Universal%20Userscripts%20Settings.js
// ==/UserScript==

(function () {
    "use strict";

    const INGEST_ENDPOINT = "https://virtupets.net/api/restock";
    const GUILDS_SETTING_KEY = "guilds";

    function parseEventType(pageHtml) {
        if (pageHtml.includes("accept your offer of")) return "bought";
        if (pageHtml.includes("has been added to your inventory")) return "bought";
        if (pageHtml.includes("is SOLD OUT!")) return "sold_out";
        return null;
    }

    function parsePrice(pageText) {
        const match = /I accept your offer of (.*?) Neopoints!'/g.exec(pageText);
        if (!match || !match[1]) return null;
        return parseInt(match[1].replaceAll(",", ""), 10);
    }

    function parseUid(url) {
        const match = /buyitem\/(.*?)\//.exec(url);
        return match ? match[1] : "unknown_uid";
    }

    function parseUsername() {
        const el = document.getElementById("user-info-username");
        return el ? el.innerText.trim() : "unknown_user";
    }

    function parseItem(pageText, eventType) {
        if (eventType === "bought") {
            const match = /Buying : (.*?)\n/.exec(pageText);
            return match ? match[1] : "unknown_item";
        }
        const soldOutMatch = /(.+?) is SOLD OUT!/.exec(pageText);
        return soldOutMatch ? soldOutMatch[1].trim() : "unknown_item";
    }

    function parseDescription() {
        const content = document.getElementById("page_content");
        const em = content ? content.getElementsByTagName("em") : [];
        return em.length > 0 ? em[0].innerText : "No description";
    }

    async function sendEvent(payload) {
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), 5000);
        try {
            const response = await fetch(INGEST_ENDPOINT, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify(payload),
                keepalive: true,
                signal: controller.signal,
            });
            if (!response.ok) {
                const body = await response.text();
                throw new Error(`Ingest failed: ${response.status} ${body}`);
            }
        } finally {
            clearTimeout(timeout);
        }
    }

    async function registerSettings() {
        if (!document.URL.includes("/help/userscripts/")) return;
        await addTextInput({
            categoryName: "Restock Logger",
            settingName: GUILDS_SETTING_KEY,
            labelText: "Guild Names",
            labelTooltip: "Add the guild names to post a log to here. Separate multiple with commas, e.g. myguild,otherguild",
            defaultSetting: "",
        });
    }

    async function main() {
        registerSettings();
        if (!document.URL.includes("/buyitem/")) return;

        const pageHtml = document.body.innerHTML;
        const pageText = document.body.innerText;
        const eventType = parseEventType(pageHtml);
        if (!eventType) return;

        const guildsRaw = await GM.getValue(GUILDS_SETTING_KEY, "");
        const guilds = guildsRaw.split(",").map(s => s.trim()).filter(Boolean);

        const payload = {
            guilds,
            eventType,
            uid: parseUid(document.URL),
            username: parseUsername(),
            item: parseItem(pageText, eventType),
            price: parsePrice(pageText),
            timestamp: new Date().toISOString(),
            description: parseDescription(),
            pageContext: {
                url: document.URL
            }
        };

        try {
            await sendEvent(payload);
        } catch (err) {
            console.error(`[RestockLogger] Failed to send event:`, err);
        }
    }

    main();
})();