Greasy Fork is available in English.
Logs restock events from Grundos Cafe to discord and virtupets.net.
// ==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();
})();