9 Commits

Author SHA1 Message Date
b35739db12 Update notify.py 2025-08-09 20:34:35 +00:00
a27c122070 Update Grepolis_Data_Sender.user.js 2025-08-08 19:26:28 +00:00
3670988ae8 Update requirements.txt 2025-08-08 18:38:42 +00:00
f3c27ae563 Add notify.py 2025-08-08 18:38:21 +00:00
28fe33a355 Update grepodata.py 2025-08-08 18:37:44 +00:00
410f266ca6 Add goldeval.py 2025-08-08 18:37:13 +00:00
65364bc889 Update Dockerfile 2025-08-08 18:36:44 +00:00
f1edc0b942 Add backup/Grepolis_Data_Sender.user.js 2025-08-08 18:35:07 +00:00
165ac08f38 Update Grepolis_Data_Sender.user.js 2025-08-07 16:36:37 +00:00
7 changed files with 436 additions and 22 deletions

View File

@@ -4,7 +4,7 @@ WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY grepodata.py .
COPY grepodata.py goldeval.py notify.py .
EXPOSE 5000
CMD ["python", "grepodata.py"]

View File

@@ -1,8 +1,8 @@
// ==UserScript==
// @name Grepolis Data Sender (Deep Scan v5.2 — Gold Sell Tab & Town Snapshot)
// @name Grepolis Data Sender (Deep Scan v6.8 — Toolbar Pause Button + Window Reopen & Full Stats)
// @namespace http://tampermonkey.net/
// @version 5.2
// @description Sends town stats always, and Premium Exchange gold market from "Sell resources for gold" tab only
// @version 6.8
// @description Sends town stats, extracts Premium Exchange title, and refreshes tab every 30s by closing and reopening Marketplace. Includes pause/resume button in top toolbar.
// @author Dimitrios
// @match https://*.grepolis.com/game/*
// @grant unsafeWindow
@@ -13,12 +13,57 @@
(function () {
'use strict';
console.log("💰 DeepScan v5.2 loaded — Town stats always, Premium gold tab if active");
console.log("💰 DeepScan v6.8 loaded — Toolbar pause button + window reopen + full stats");
const uw = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
let latestBasicPayload = null;
let paused = false;
// 💱 Extract data ONLY from "Sell resources for gold" tab
// 🧠 Toolbar button HTML
const deepScanButtonHtml =
'<div class="divider"></div>' +
'<div class="activity" id="dsbutton" style="filter: brightness(70%) sepia(100%) hue-rotate(-50deg) saturate(1000%) contrast(0.8);">' +
'<p id="dslabel" style="z-index:6; top:-8px; position:relative; font-weight:bold;">DeepScan</p>' +
'</div>';
// 🧠 Toggle pause/resume
function toggleDeepScanButton() {
paused = !paused;
const label = document.getElementById('dslabel');
const btn = document.getElementById('dsbutton');
if (paused) {
label.textContent = 'Paused';
btn.style.filter = 'brightness(70%) sepia(100%) hue-rotate(-50deg) saturate(1000%) contrast(0.8)';
} else {
label.textContent = 'DeepScan';
btn.style.filter = 'brightness(294%) sepia(100%) hue-rotate(15deg) saturate(1000%) contrast(0.8)';
}
console.log(`🔘 DeepScan is now ${paused ? 'paused' : 'running'}`);
}
// 🧠 Inject button into toolbar
setTimeout(() => {
if (!document.getElementById('dsbutton')) {
uw.$('.tb_activities, .toolbar_activities').find('.middle').append(deepScanButtonHtml);
}
}, 4000);
uw.$(document).on('click', '#dsbutton', toggleDeepScanButton);
// 📌 Extract Premium Exchange title
function getGoldMarketTitle() {
const titleEl = document.querySelector(
".classic_window.market #premium_exchange .game_border_header .title.exchange_title"
);
const titleText = titleEl?.textContent.trim() || "";
console.log("📌 Gold Market Title:", titleText);
return titleText;
}
// 📊 Extract gold market data
function scrapeGoldMarketExchange() {
const exchange = {};
const resourceTypes = ["wood", "stone", "iron"];
@@ -50,8 +95,13 @@
return exchange;
}
// 📦 Send all town stats — always
// 📤 Send town + gold data
function sendBasicStats() {
if (paused) {
console.log("⏸️ DeepScan is paused — skipping data send");
return;
}
try {
const towns = uw.ITowns?.towns || {};
const player = uw.Game?.player_name || "unknown";
@@ -77,7 +127,8 @@
};
});
const goldMarket = scrapeGoldMarketExchange(); // may return null
const goldMarket = scrapeGoldMarketExchange();
const goldTitle = getGoldMarketTitle();
latestBasicPayload = {
type: "basic",
@@ -85,9 +136,12 @@
player,
player_id,
towns: stats,
gold_market: goldMarket || {} // always include field
gold_market: goldMarket || {},
gold_market_title: goldTitle || ""
};
console.log("📤 Sending payload:", latestBasicPayload);
fetch("https://grepo.haunter-pets.top/api/grepolis-data", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -97,20 +151,77 @@
.then(txt => console.log("✅ Data sent:", txt))
.catch(err => console.error("❌ Failed to send:", err));
} catch (e) {
console.error("💥 Error sending data:", e);
console.error("💥 Error sending stats:", e);
}
}
// ⏱️ Timer: always sends town info; adds gold data only if Sell tab active
function runDataSendInterval() {
// ❎ Close all Marketplace windows
function forceCloseMarketplaceWindows() {
const marketWindows = document.querySelectorAll(".classic_window.market");
marketWindows.forEach(win => {
const closeBtn = win.querySelector(".buttons_container .btn_wnd.close");
if (closeBtn) {
closeBtn.click();
console.log("❎ Clicked close button for Marketplace window:", win.id);
} else {
console.warn("⚠️ Close button not found in window:", win.id);
}
});
}
// 🏛️ Refresh Premium Exchange tab by closing and reopening
function refreshGoldWindow() {
if (paused) {
console.log("⏸️ DeepScan is paused — skipping refresh");
return;
}
try {
forceCloseMarketplaceWindows();
setTimeout(() => {
const marketArea = document.querySelector("area#building_main_area_market");
if (marketArea) {
marketArea.dispatchEvent(new MouseEvent("click", { bubbles: true }));
console.log("🏛️ Clicked Marketplace area to reopen Premium Exchange");
setTimeout(() => {
const newWin = Array.from(document.querySelectorAll(".classic_window.market"))
.find(win => win.querySelector("#premium_exchange"));
if (newWin) {
const sellTab = newWin.querySelector(".gp_page_caption.js-page-caption-1");
if (sellTab) {
sellTab.click();
console.log("🔁 Switched to Premium Exchange Sell tab");
} else {
console.warn("⚠️ Sell tab not found in reopened window");
}
} else {
console.warn("❌ Premium Exchange window not found after reopening");
}
}, 1500);
} else {
console.warn("❌ Marketplace area not found — cannot reopen Premium Exchange");
}
}, 5000);
} catch (err) {
console.error("💥 Error in refreshGoldWindow:", err);
}
}
// 🔁 Main loop
function runDataCycle() {
setInterval(() => {
console.log("🕒 Snapshot triggered — scanning town and gold sell tab...");
sendBasicStats();
console.log("🕒 Data cycle tick — close/reopen + send stats");
refreshGoldWindow();
setTimeout(sendBasicStats, 7000); // after reopen + tab switch
}, 30000);
}
window.addEventListener("load", () => {
console.log("🚀 Grepolis page ready — DeepScan v5.2 booting...");
runDataSendInterval();
console.log("🚀 DeepScan v6.8 booting up...");
runDataCycle();
});
})();

View File

@@ -0,0 +1,209 @@
// ==UserScript==
// @name Grepolis Data Sender (Deep Scan v6.7 — Toolbar Pause Button + Soft Refresh & Full Stats)
// @namespace http://tampermonkey.net/
// @version 6.7
// @description Sends town stats, extracts Premium Exchange title, and soft-refreshes tab every 30s via Marketplace building click. Includes pause/resume button in top toolbar.
// @author Dimitrios
// @match https://*.grepolis.com/game/*
// @grant unsafeWindow
// @updateURL https://git.haunter-pets.top/haunter/grepodata/raw/branch/main/Grepolis_Data_Sender.user.js
// @downloadURL https://git.haunter-pets.top/haunter/grepodata/raw/branch/main/Grepolis_Data_Sender.user.js
// ==/UserScript==
(function () {
'use strict';
console.log("💰 DeepScan v6.7 loaded — Soft refresh + Toolbar pause button + full stats");
const uw = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
let latestBasicPayload = null;
let paused = false;
// 🧠 Toolbar button HTML
const deepScanButtonHtml =
'<div class="divider"></div>' +
'<div class="activity" id="dsbutton" style="filter: brightness(70%) sepia(100%) hue-rotate(-50deg) saturate(1000%) contrast(0.8);">' +
'<p id="dslabel" style="z-index:6; top:-8px; position:relative; font-weight:bold;">DeepScan</p>' +
'</div>';
// 🧠 Toggle pause/resume
function toggleDeepScanButton() {
paused = !paused;
const label = document.getElementById('dslabel');
const btn = document.getElementById('dsbutton');
if (paused) {
label.textContent = 'Paused';
btn.style.filter = 'brightness(70%) sepia(100%) hue-rotate(-50deg) saturate(1000%) contrast(0.8)';
} else {
label.textContent = 'DeepScan';
btn.style.filter = 'brightness(294%) sepia(100%) hue-rotate(15deg) saturate(1000%) contrast(0.8)';
}
console.log(`🔘 DeepScan is now ${paused ? 'paused' : 'running'}`);
}
// 🧠 Inject button into toolbar
setTimeout(() => {
if (!document.getElementById('dsbutton')) {
uw.$('.tb_activities, .toolbar_activities').find('.middle').append(deepScanButtonHtml);
}
}, 4000);
uw.$(document).on('click', '#dsbutton', toggleDeepScanButton);
// 📌 Extract Premium Exchange title
function getGoldMarketTitle() {
const titleEl = document.querySelector(
".classic_window.market #premium_exchange .game_border_header .title.exchange_title"
);
const titleText = titleEl?.textContent.trim() || "";
console.log("📌 Gold Market Title:", titleText);
return titleText;
}
// 📊 Extract gold market data
function scrapeGoldMarketExchange() {
const exchange = {};
const resourceTypes = ["wood", "stone", "iron"];
const isSellTabActive =
document.querySelector("#premium_exchange .gp_page_caption.js-page-caption-1.active") &&
document.querySelector("#premium_exchange .gp_tab_page.js-page-1.active");
if (!isSellTabActive) {
console.warn("🚫 Sell tab not active — skipping gold extraction");
return null;
}
const blocks = document.querySelectorAll(
"#premium_exchange .gp_tab_page.js-page-1.active .resources_wrapper .resource"
);
blocks.forEach((block, i) => {
const type = resourceTypes[i] || `res_${i}`;
const currentEl = block.querySelector(".progressbar_bg .caption span.current");
const maxEl = block.querySelector(".progressbar_bg .caption span.max");
exchange[type] = {
current: currentEl?.textContent.trim() || "0",
max: maxEl?.textContent.trim() || "0"
};
});
console.log("📊 Gold Market Sell Tab data:", exchange);
return exchange;
}
// 📤 Send town + gold data
function sendBasicStats() {
if (paused) {
console.log("⏸️ DeepScan is paused — skipping data send");
return;
}
try {
const towns = uw.ITowns?.towns || {};
const player = uw.Game?.player_name || "unknown";
const player_id = uw.Game?.player_id || "unknown";
const stats = Object.values(towns).map(town => {
const res = town.resources();
const buildings = town.buildings()?.attributes ?? {};
const marketLevel = typeof town.getMarketplaceLevel === "function"
? town.getMarketplaceLevel()
: buildings.market || null;
return {
town_id: town.id,
town_name: town.name,
wood: res.wood,
stone: res.stone,
iron: res.iron,
population: res.population,
points: town.getPoints(),
buildings,
market: { level: marketLevel }
};
});
const goldMarket = scrapeGoldMarketExchange();
const goldTitle = getGoldMarketTitle();
latestBasicPayload = {
type: "basic",
timestamp: new Date().toISOString(),
player,
player_id,
towns: stats,
gold_market: goldMarket || {},
gold_market_title: goldTitle || ""
};
console.log("📤 Sending payload:", latestBasicPayload);
fetch("https://grepo.haunter-pets.top/api/grepolis-data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(latestBasicPayload)
})
.then(res => res.text())
.then(txt => console.log("✅ Data sent:", txt))
.catch(err => console.error("❌ Failed to send:", err));
} catch (e) {
console.error("💥 Error sending stats:", e);
}
}
// 🏛️ Soft refresh: click building and switch tab
function refreshGoldWindow() {
if (paused) {
console.log("⏸️ DeepScan is paused — skipping refresh");
return;
}
try {
const marketArea = document.querySelector("area#building_main_area_market");
if (marketArea) {
marketArea.dispatchEvent(new MouseEvent("click", { bubbles: true }));
console.log("🏛️ Clicked Marketplace area to trigger Premium Exchange");
// Wait 1.5s and switch to Sell tab
setTimeout(() => {
const newWin = Array.from(document.querySelectorAll(".classic_window.market"))
.find(win => win.querySelector("#premium_exchange"));
if (newWin) {
const sellTab = newWin.querySelector(".gp_page_caption.js-page-caption-1");
if (sellTab) {
sellTab.click();
console.log("🔁 Switched to Premium Exchange Sell tab");
} else {
console.warn("⚠️ Sell tab not found in window");
}
} else {
console.warn("❌ Premium Exchange window not found after click");
}
}, 1500);
} else {
console.warn("❌ Marketplace area not found — cannot trigger Premium Exchange");
}
} catch (err) {
console.error("💥 Error in refreshGoldWindow:", err);
}
}
// 🔁 Main loop
function runDataCycle() {
setInterval(() => {
console.log("🕒 Data cycle tick — soft refresh + send stats");
refreshGoldWindow();
setTimeout(sendBasicStats, 3000);
}, 30000);
}
window.addEventListener("load", () => {
console.log("🚀 DeepScan v6.5 booting up...");
runDataCycle();
});
})();

30
goldeval.py Normal file
View File

@@ -0,0 +1,30 @@
import json
import os
from notify import hooknotify
def goldeval(player_file):
"""Evaluate gold market surplus and send alerts if needed."""
if not os.path.exists(player_file):
print(f"❌ File not found: {player_file}")
return
try:
with open(player_file, "r", encoding="utf-8") as f:
data = json.load(f)
title = data.get("gold_market_title", "Unknown Title")
timestamp = data.get("server_received_at", "Unknown Time")
market = data.get("gold_market", {})
for resource in ["wood", "stone", "iron"]:
res_data = market.get(resource, {})
current = int(res_data.get("current", "0").replace(",", ""))
max_val = int(res_data.get("max", "0").replace(",", ""))
surplus = max_val - current
if surplus > 1000:
print(f"📢 Surplus detected: {resource} = {surplus}")
hooknotify(timestamp, title, resource, surplus)
except Exception as e:
print("💥 Error in goldeval:", e)

View File

@@ -1,9 +1,9 @@
from flask import Flask, request, jsonify
from flask_cors import CORS
import json
import os
import re
from datetime import datetime # ✅ minimal timestamp support
from datetime import datetime
from goldeval import goldeval # ⬅️ Add this at the top
app = Flask(__name__)
CORS(app)
@@ -25,6 +25,45 @@ def save_payload_as_player_file(payload):
print(f"📝 Payload saved to {filename}")
except Exception as e:
print("❌ Failed to save payload:", e)
goldeval(filename)
def append_prices_log(payload):
"""Append resource prices and market title to prices.txt in CSV format with timestamp."""
try:
market = payload.get("gold_market")
title = payload.get("gold_market_title", "").strip()
# Skip if title is missing, empty, or placeholder
if not title or title.lower() in ["unknown title", "n/a", "null"]:
print("⏭️ Skipping log: Missing or placeholder title")
return
# Skip if market is missing
if not market:
print("⏭️ Skipping log: Missing market data")
return
# Extract and sanitize values
wood = market.get("wood", {}).get("current", "").replace(",", "")
stone = market.get("stone", {}).get("current", "").replace(",", "")
iron = market.get("iron", {}).get("current", "").replace(",", "")
# Skip if all values are missing or zero
if not wood or not stone or not iron or (wood == "0" and stone == "0" and iron == "0"):
print("⏭️ Skipping log: Market values are empty or zero")
return
timestamp = datetime.now().isoformat()
safe_title = title.replace('"', "'")
line = f'{timestamp},{wood},{stone},{iron},"{safe_title}"\n'
with open("prices.txt", "a", encoding="utf-8") as f:
f.write(line)
print("📈 Appended to prices.txt:", line.strip())
except Exception as log_err:
print("⚠️ Failed to log to prices.txt:", log_err)
@app.route('/api/grepolis-data', methods=['POST'])
def receive_data():
@@ -34,10 +73,11 @@ def receive_data():
payload = request.get_json(force=True)
print("✅ Parsed JSON:", payload)
# Inject server-side timestamp (system time)
# Inject server-side timestamp
payload["server_received_at"] = datetime.now().isoformat()
save_payload_as_player_file(payload)
append_prices_log(payload)
return jsonify({
"status": "success",

23
notify.py Normal file
View File

@@ -0,0 +1,23 @@
import requests
# ✅ Your Discord webhook URL
WEBHOOK_URL2 = "https://discord.com/api/webhooks/847569410839150662/lJo0y5aah72zj-ML-9jMMAEPzD5M0gafC4xUe2d5HlKZaW0Z187wjCW912fmJqV5c74a"
WEBHOOK_URL = "https://discord.com/api/webhooks/1403835326509744211/OjqbGjCvwAEDfWSqAU-YVcqcgsT4d9U84oEM4a89Ec7Jsi5GO_jIg8zl9nwB9KDB0gmr"
def hooknotify(server_time, title, resource, surplus):
"""Send Discord notification with surplus info."""
message = (
"```yaml\n"
f"{server_time} | {title} | {resource} surplus: {surplus}\n"
"```"
)
data = { "content": message }
try:
response = requests.post(WEBHOOK_URL2, json=data)
response = requests.post(WEBHOOK_URL, json=data)
if response.status_code == 204:
print(f"📨 Notification sent for {resource}")
else:
print(f"⚠️ Failed to send notification: {response.status_code}")
except Exception as e:
print("❌ Error sending notification:", e)

View File

@@ -1,2 +1,3 @@
Flask==3.0.3
flask-cors==4.0.0
requests