continente-mcp 3.1.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ export const normalizeFavoriteId = value => String(value ?? '').replace(/\.html$/, '');
2
+
3
+ export function createQueue() {
4
+ let tail = Promise.resolve();
5
+ return operation => {
6
+ const result = tail.then(operation);
7
+ tail = result.catch(() => {});
8
+ return result;
9
+ };
10
+ }
11
+
12
+ export function validateToolInput(name, args = {}) {
13
+ if (!args || typeof args !== 'object' || Array.isArray(args)) throw new Error('Invalid arguments');
14
+ if (['add_to_cart', 'update_cart_item'].includes(name)) {
15
+ if (typeof args.product_id !== 'string' || args.product_id.length > 250 ||
16
+ !/^(?:[a-zA-Z0-9]+-)*\d+(?:-master)?(?:\.html)?$/.test(args.product_id)) {
17
+ throw new Error('Invalid product_id: expected a numeric PID or product slug ending in a numeric PID');
18
+ }
19
+ const quantity = args.quantity === undefined && name === 'add_to_cart' ? 1 : args.quantity;
20
+ if (typeof quantity !== 'number' || !Number.isFinite(quantity) || quantity < 0 ||
21
+ (name === 'add_to_cart' && quantity === 0)) {
22
+ throw new Error(name === 'add_to_cart'
23
+ ? 'Quantity must be finite and greater than zero'
24
+ : 'Quantity must be finite and non-negative; zero removes the item');
25
+ }
26
+ }
27
+ if (['search_products', 'get_order_history', 'get_most_bought'].includes(name) && args.limit !== undefined &&
28
+ (!Number.isInteger(args.limit) || args.limit < 1 || args.limit > 50)) {
29
+ throw new Error('Limit must be an integer from 1 to 50');
30
+ }
31
+ if (name === 'search_products' &&
32
+ (typeof args.query !== 'string' || !args.query.trim() || args.query.length > 250 || /[\x00-\x1f\x7f]/.test(args.query))) {
33
+ throw new Error('Query must be nonempty text, at most 250 characters, without control characters');
34
+ }
35
+ return args;
36
+ }
37
+
38
+ export async function retryAuthentication(operation, refresh, mutation = false) {
39
+ const first = await operation();
40
+ if (first?.error !== 'not_authenticated' || first.writeAttempted === true ||
41
+ (mutation && first.writeAttempted !== false)) return first;
42
+ if (!(await refresh())) return first;
43
+ return operation();
44
+ }
package/check-change.js DELETED
@@ -1,157 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Continente Change Detector
4
- *
5
- * Checks whether the website structure has changed in a way that might break
6
- * the MCP server's scrapers. Run daily via cron.
7
- *
8
- * Usage:
9
- * node check-change.js
10
- *
11
- * On first run it saves a baseline fingerprint. Subsequent runs compare against
12
- * it and log a warning if the structure changed significantly.
13
- *
14
- * To receive a webhook notification on change, set:
15
- * CONTINENTE_ALERT_WEBHOOK=https://hooks.slack.com/...
16
- */
17
-
18
- import { createHash } from 'crypto';
19
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
20
- import { chromium } from 'playwright';
21
- import * as cheerio from 'cheerio';
22
- import os from 'os';
23
-
24
- const CONTINENTE_SEARCH = 'https://www.continente.pt/pesquisa/?q=leite';
25
- const STATE_DIR = `${os.homedir()}/.continente`;
26
- const FINGERPRINT_FILE = `${STATE_DIR}/fingerprint.json`;
27
- const LAST_ALERT_FILE = `${STATE_DIR}/last_change_alert.txt`;
28
- const COOKIE_FILE = `${STATE_DIR}/cookies.json`;
29
- const WEBHOOK_URL = process.env.CONTINENTE_ALERT_WEBHOOK || null;
30
-
31
- function getPlatformUserAgent() {
32
- if (process.platform === 'win32') {
33
- return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36';
34
- } else if (process.platform === 'linux') {
35
- return 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36';
36
- }
37
- return 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36';
38
- }
39
-
40
- function extractFingerprint(html) {
41
- const $ = cheerio.load(html);
42
- $('script, style, noscript, iframe, svg').remove();
43
-
44
- const counts = {
45
- produtoLinks: $('a[href*="/produto/"]').length,
46
- priceElements: $('[class*="price"]').filter((i, el) => $(el).text().includes('€')).length,
47
- forms: $('form').length,
48
- buttons: $('button').length,
49
- productCards: $('[class*="product"], [class*="card"]').filter(
50
- (i, el) => $(el).find('a[href*="/produto/"]').length > 0
51
- ).length,
52
- };
53
-
54
- return {
55
- ...counts,
56
- hash: createHash('sha256').update(JSON.stringify(counts)).digest('hex').substring(0, 12),
57
- timestamp: new Date().toISOString(),
58
- };
59
- }
60
-
61
- async function fetchHtml() {
62
- const browser = await chromium.launch({ headless: true });
63
- const context = await browser.newContext({ userAgent: getPlatformUserAgent() });
64
-
65
- if (existsSync(COOKIE_FILE)) {
66
- try {
67
- const cookies = JSON.parse(readFileSync(COOKIE_FILE, 'utf8'));
68
- if (cookies.length > 0) await context.addCookies(cookies);
69
- } catch {}
70
- }
71
-
72
- const page = await context.newPage();
73
- await page.goto(CONTINENTE_SEARCH, { waitUntil: 'domcontentloaded', timeout: 20000 });
74
- await page.waitForTimeout(3000);
75
-
76
- const html = await page.content();
77
- await browser.close();
78
- return html;
79
- }
80
-
81
- async function sendAlert(message) {
82
- if (WEBHOOK_URL) {
83
- // POST to webhook (works with Slack, Discord, n8n, etc.)
84
- try {
85
- const res = await fetch(WEBHOOK_URL, {
86
- method: 'POST',
87
- headers: { 'Content-Type': 'application/json' },
88
- body: JSON.stringify({ text: message }),
89
- });
90
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
91
- console.error('[ChangeDetector] Webhook notified.');
92
- } catch (e) {
93
- console.error(`[ChangeDetector] Webhook failed: ${e.message}`);
94
- }
95
- } else {
96
- // No webhook configured — log prominently to stdout so cron picks it up
97
- console.log('\n' + '='.repeat(60));
98
- console.log('CONTINENTE STRUCTURE CHANGE DETECTED');
99
- console.log('='.repeat(60));
100
- console.log(message);
101
- console.log('='.repeat(60) + '\n');
102
- }
103
- }
104
-
105
- async function main() {
106
- mkdirSync(STATE_DIR, { recursive: true });
107
-
108
- console.error(`[ChangeDetector] Checking ${CONTINENTE_SEARCH}`);
109
- const html = await fetchHtml();
110
- const current = extractFingerprint(html);
111
-
112
- if (!existsSync(FINGERPRINT_FILE)) {
113
- writeFileSync(FINGERPRINT_FILE, JSON.stringify({ baseline: current, savedAt: current.timestamp }, null, 2));
114
- console.error(`[ChangeDetector] Baseline saved (${current.hash}). Run again tomorrow to start detecting changes.`);
115
- return;
116
- }
117
-
118
- const { baseline } = JSON.parse(readFileSync(FINGERPRINT_FILE, 'utf8'));
119
-
120
- if (baseline.hash === current.hash) {
121
- console.error(`[ChangeDetector] Unchanged (${current.hash}). All good.`);
122
- return;
123
- }
124
-
125
- const diffs = {};
126
- for (const key of Object.keys(baseline)) {
127
- if (key === 'hash' || key === 'timestamp') continue;
128
- if (JSON.stringify(baseline[key]) !== JSON.stringify(current[key])) {
129
- diffs[key] = { before: baseline[key], after: current[key] };
130
- }
131
- }
132
-
133
- const diffSummary = Object.entries(diffs).map(([k, v]) => `${k}: ${v.before} → ${v.after}`).join(', ');
134
- console.error(`[ChangeDetector] CHANGE DETECTED: ${diffSummary}`);
135
-
136
- // Rate-limit: only alert once per 24 h
137
- if (existsSync(LAST_ALERT_FILE)) {
138
- const lastAlert = readFileSync(LAST_ALERT_FILE, 'utf8').trim();
139
- const hoursSince = (Date.now() - new Date(lastAlert).getTime()) / 3_600_000;
140
- if (hoursSince < 24) {
141
- console.error(`[ChangeDetector] Alert suppressed (last sent ${hoursSince.toFixed(1)}h ago).`);
142
- return;
143
- }
144
- }
145
-
146
- const alertMsg =
147
- `⚠️ Continente website structure changed — the MCP scraper may need updating.\n\n` +
148
- `Baseline: ${baseline.hash}\n` +
149
- `Current: ${current.hash}\n` +
150
- `Changes: ${diffSummary}\n\n` +
151
- `Check https://github.com/davidmarqu3s/continente-mcp for updates.`;
152
-
153
- await sendAlert(alertMsg);
154
- writeFileSync(LAST_ALERT_FILE, new Date().toISOString());
155
- }
156
-
157
- main().catch(e => { console.error('[ChangeDetector] Fatal:', e.message); process.exit(1); });
@@ -1,166 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Continente Order Backup — incremental
4
- *
5
- * Checks for orders not yet in the backup, fetches their details,
6
- * and appends them. Run daily via cron.
7
- *
8
- * Usage:
9
- * node continente-backup.js
10
- *
11
- * Output:
12
- * ~/.continente/orders-backup.json
13
- */
14
- import { chromium } from 'playwright';
15
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
16
- import { normalizeCookies } from './src/utils.js';
17
- import os from 'os';
18
-
19
- const CONTINENTE_BASE = 'https://www.continente.pt';
20
- const STATE_DIR = `${os.homedir()}/.continente`;
21
- const BACKUP_FILE = `${STATE_DIR}/orders-backup.json`;
22
- const COOKIE_FILE = `${STATE_DIR}/cookies.json`;
23
-
24
- const log = (...args) => console.error('[backup]', ...args);
25
-
26
- // ─── Cookie helpers ───────────────────────────────────────────────────────────
27
-
28
- function loadCookies() {
29
- if (!existsSync(COOKIE_FILE)) {
30
- log(`No cookies found at ${COOKIE_FILE}`);
31
- log('Run: python3 continente-cookie-reader.py');
32
- return null;
33
- }
34
- try {
35
- return JSON.parse(readFileSync(COOKIE_FILE, 'utf8'));
36
- } catch (e) {
37
- log('Failed to parse cookies:', e.message);
38
- return null;
39
- }
40
- }
41
-
42
- // ─── Scraping helpers ─────────────────────────────────────────────────────────
43
-
44
- async function getOrderLinks(page) {
45
- await page.goto(`${CONTINENTE_BASE}/conta/encomendas/`, { waitUntil: 'networkidle', timeout: 30000 });
46
- await page.waitForTimeout(3000);
47
-
48
- if (page.url().includes('/login')) throw new Error('Not authenticated — cookies may have expired');
49
-
50
- return page.evaluate(() => {
51
- const seen = new Set();
52
- return Array.from(document.querySelectorAll('a[href*="detalhe-encomenda"]'))
53
- .map(a => a.href)
54
- .filter(h => { if (seen.has(h)) return false; seen.add(h); return true; });
55
- });
56
- }
57
-
58
- async function getOrderDetail(page, url) {
59
- await page.goto(url, { waitUntil: 'networkidle', timeout: 20000 });
60
- await page.waitForTimeout(2000);
61
-
62
- return page.evaluate(() => {
63
- const bodyText = document.body.innerText;
64
- const orderNumMatch = bodyText.match(/Encomenda (\d{9}_\d{3})/);
65
- const totalMatch = bodyText.match(/Total Encomendado\s+([\d,.]+€)/);
66
- const slotMatch = bodyText.match(/(\d{1,2}:\d{2} - \d{1,2}:\d{2})/);
67
- const dateMatch = bodyText.match(/(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2},\s+\d{4}/i);
68
-
69
- const products = [];
70
- document.querySelectorAll('[class*="product-line"]').forEach(el => {
71
- const qtyEl = el.querySelector('[class*="qty"], [class*="quantity"], [class*="amount"]');
72
- if (!qtyEl) return;
73
- const nameEl = el.querySelector('[class*="product-name"], [class*="name"], a[href*="produto"]');
74
- const rawName = (nameEl || el).textContent.trim();
75
- const name = rawName.split('\n')[0].trim();
76
- const qtyMatch = qtyEl.textContent.trim().match(/^(\d+)/);
77
- const qty = qtyMatch ? parseInt(qtyMatch[1], 10) : 1;
78
- const priceEl = el.querySelector('[class*="price"]');
79
- const price = priceEl ? priceEl.textContent.trim().split('\n')[0].trim() : null;
80
- if (name && name.length > 3) products.push({ name, qty, price });
81
- });
82
-
83
- return {
84
- orderNumber: orderNumMatch ? orderNumMatch[1] : null,
85
- date: dateMatch ? dateMatch[0] : null,
86
- total: totalMatch ? totalMatch[1] : null,
87
- deliverySlot: slotMatch ? slotMatch[1] : null,
88
- productCount: products.length,
89
- products,
90
- };
91
- });
92
- }
93
-
94
- // ─── Main ─────────────────────────────────────────────────────────────────────
95
-
96
- async function main() {
97
- const cookies = loadCookies();
98
- if (!cookies) process.exit(1);
99
-
100
- // Load existing backup
101
- let backup = { backedUpAt: null, orderCount: 0, orders: [] };
102
- if (existsSync(BACKUP_FILE)) {
103
- try { backup = JSON.parse(readFileSync(BACKUP_FILE, 'utf8')); } catch {}
104
- }
105
- const knownUrls = new Set(backup.orders.map(o => o.url).filter(Boolean));
106
-
107
- const browser = await chromium.launch({ headless: true });
108
- const context = await browser.newContext({
109
- userAgent: getPlatformUserAgent(),
110
- locale: 'pt-PT',
111
- });
112
- await context.addCookies(normalizeCookies(cookies));
113
- const page = await context.newPage();
114
-
115
- let orderLinks;
116
- try {
117
- orderLinks = await getOrderLinks(page);
118
- } catch (e) {
119
- log('Failed to load orders page:', e.message);
120
- await browser.close();
121
- process.exit(1);
122
- }
123
-
124
- const newLinks = orderLinks.filter(l => !knownUrls.has(l));
125
- log(`${orderLinks.length} orders on site, ${knownUrls.size} already backed up, ${newLinks.length} new`);
126
-
127
- if (newLinks.length === 0) {
128
- log('Nothing new to backup.');
129
- await browser.close();
130
- return;
131
- }
132
-
133
- const newOrders = [];
134
- for (let i = 0; i < newLinks.length; i++) {
135
- try {
136
- const detail = await getOrderDetail(page, newLinks[i]);
137
- detail.url = newLinks[i];
138
- newOrders.push(detail);
139
- log(`${i + 1}/${newLinks.length} — ${detail.orderNumber || '?'} (${detail.productCount} products)`);
140
- } catch (e) {
141
- log(`${i + 1}/${newLinks.length} FAILED: ${e.message}`);
142
- newOrders.push({ url: newLinks[i], error: e.message });
143
- }
144
- }
145
-
146
- backup.orders = [...newOrders, ...backup.orders];
147
- backup.orderCount = backup.orders.filter(o => !o.error).length;
148
- backup.backedUpAt = new Date().toISOString();
149
-
150
- mkdirSync(STATE_DIR, { recursive: true });
151
- writeFileSync(BACKUP_FILE, JSON.stringify(backup, null, 2));
152
- log(`Done — added ${newOrders.length} orders. Total: ${backup.orderCount} → ${BACKUP_FILE}`);
153
-
154
- await browser.close();
155
- }
156
-
157
- function getPlatformUserAgent() {
158
- if (process.platform === 'win32') {
159
- return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36';
160
- } else if (process.platform === 'linux') {
161
- return 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36';
162
- }
163
- return 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36';
164
- }
165
-
166
- main().catch(e => { log('Fatal:', e.message); process.exit(1); });
@@ -1,164 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- continente-cookie-reader — export Continente.pt cookies from your browser.
4
-
5
- Reads browser cookies for continente.pt and writes them to ~/.continente/cookies.json
6
- so the MCP server can use them. Auto-detects your browser or let you specify one.
7
-
8
- Usage:
9
- python3 continente-cookie-reader.py # auto-detect
10
- python3 continente-cookie-reader.py --browser chrome
11
- python3 continente-cookie-reader.py --browser firefox
12
- python3 continente-cookie-reader.py --output /path/to/cookies.json
13
- python3 continente-cookie-reader.py --list-browsers
14
-
15
- Requirements:
16
- pip install browser-cookie3
17
-
18
- Environment variables:
19
- CONTINENTE_COOKIES_PATH Override the default output path (~/.continente/cookies.json)
20
- CONTINENTE_VAULT_COOKIE_PATH If set, also write cookies to this path (useful for syncing
21
- to a second machine via a shared folder, e.g. Obsidian vault)
22
- """
23
- import argparse
24
- import json
25
- import os
26
- import stat
27
- import sys
28
- from pathlib import Path
29
-
30
- DOMAIN = "continente.pt"
31
- DEFAULT_OUTPUT = Path(
32
- os.environ.get("CONTINENTE_COOKIES_PATH", str(Path.home() / ".continente" / "cookies.json"))
33
- )
34
-
35
- # If set, cookies are also written here (e.g. for syncing to a remote machine)
36
- VAULT_PATH = Path(os.environ["CONTINENTE_VAULT_COOKIE_PATH"]) if "CONTINENTE_VAULT_COOKIE_PATH" in os.environ else None
37
-
38
- # Tried in this order when auto-detecting
39
- BROWSERS = ["arc", "chrome", "edge", "brave", "firefox", "chromium", "vivaldi", "opera", "safari"]
40
-
41
-
42
- def get_browser_cookies(browser_name: str) -> list[dict]:
43
- """Read continente.pt cookies from the given browser."""
44
- import browser_cookie3
45
-
46
- getter = getattr(browser_cookie3, browser_name, None)
47
- if getter is None:
48
- raise ValueError(f"Browser '{browser_name}' not supported by browser-cookie3.")
49
-
50
- jar = getter(domain_name=DOMAIN)
51
- cookies = []
52
- for c in jar:
53
- cookies.append({
54
- "domain": c.domain,
55
- "name": c.name,
56
- "value": c.value,
57
- "path": getattr(c, "path", "/"),
58
- "secure": bool(getattr(c, "secure", False)),
59
- "httpOnly": c.has_nonstandard_attr("HttpOnly") if hasattr(c, "has_nonstandard_attr") else False,
60
- "sameSite": "Lax",
61
- "expires": getattr(c, "expires", None),
62
- })
63
- return cookies
64
-
65
-
66
- def find_cookies(browser: str | None) -> tuple[list[dict], str]:
67
- """Return (cookies, browser_name). Tries specified browser or auto-detects."""
68
- if browser:
69
- try:
70
- cookies = get_browser_cookies(browser)
71
- except Exception as e:
72
- print(f"Error reading {browser}: {e}", file=sys.stderr)
73
- sys.exit(1)
74
- if not cookies:
75
- print(f"No Continente cookies found in {browser}. Are you logged in?", file=sys.stderr)
76
- sys.exit(1)
77
- return cookies, browser
78
-
79
- print("Auto-detecting browser with Continente cookies...", file=sys.stderr)
80
- for name in BROWSERS:
81
- try:
82
- cookies = get_browser_cookies(name)
83
- if cookies:
84
- return cookies, name
85
- except Exception as e:
86
- print(f" {name}: {e}", file=sys.stderr)
87
- continue
88
-
89
- print(
90
- "\nNo Continente cookies found in any supported browser.\n"
91
- "Make sure you are logged into continente.pt, then try again.\n"
92
- f"Supported browsers: {', '.join(BROWSERS)}",
93
- file=sys.stderr,
94
- )
95
- sys.exit(1)
96
-
97
-
98
- def write_cookie_file(cookies: list[dict], path: Path) -> None:
99
- """Write cookies JSON and set owner-only permissions."""
100
- path.parent.mkdir(parents=True, exist_ok=True)
101
- path.write_text(json.dumps(cookies, indent=2, ensure_ascii=False))
102
- os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) # 600 — cookies = full account access
103
-
104
-
105
- def main() -> None:
106
- parser = argparse.ArgumentParser(
107
- description="Export Continente.pt browser cookies for use with continente-mcp.",
108
- formatter_class=argparse.RawDescriptionHelpFormatter,
109
- )
110
- parser.add_argument(
111
- "--browser", "-b",
112
- choices=BROWSERS,
113
- metavar="BROWSER",
114
- help=f"Browser to read from (default: auto-detect). One of: {', '.join(BROWSERS)}",
115
- )
116
- parser.add_argument(
117
- "--output", "-o",
118
- type=Path,
119
- default=DEFAULT_OUTPUT,
120
- help=f"Output path (default: {DEFAULT_OUTPUT})",
121
- )
122
- parser.add_argument(
123
- "--list-browsers",
124
- action="store_true",
125
- help="List supported browsers and exit",
126
- )
127
- args = parser.parse_args()
128
-
129
- if args.list_browsers:
130
- print("Supported browsers: " + ", ".join(BROWSERS))
131
- sys.exit(0)
132
-
133
- try:
134
- import browser_cookie3 # noqa: F401
135
- except ImportError:
136
- print(
137
- "browser-cookie3 is not installed.\n"
138
- "Run: pip install browser-cookie3",
139
- file=sys.stderr,
140
- )
141
- sys.exit(1)
142
-
143
- cookies, browser_name = find_cookies(args.browser)
144
-
145
- write_cookie_file(cookies, args.output)
146
- print(f"Saved {len(cookies)} cookies → {args.output} (browser: {browser_name})")
147
-
148
- # Optional: also write to a secondary path (e.g. for syncing to a remote machine)
149
- if VAULT_PATH:
150
- existing = []
151
- if VAULT_PATH.exists():
152
- try:
153
- existing = json.loads(VAULT_PATH.read_text())
154
- except Exception:
155
- pass
156
- if cookies != existing:
157
- write_cookie_file(cookies, VAULT_PATH)
158
- print(f"Saved {len(cookies)} cookies → {VAULT_PATH} (vault sync)")
159
- else:
160
- print(f"Vault unchanged — skipping write to {VAULT_PATH}")
161
-
162
-
163
- if __name__ == "__main__":
164
- main()
@@ -1,135 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Continente Session Keepalive
4
-
5
- Pings continente.pt to reset the idle session timeout.
6
- If the session has expired, re-reads cookies from your browser automatically.
7
-
8
- Run every 20 minutes via cron or launchd:
9
- */20 * * * * python3 /path/to/continente-keepalive.py >> ~/.continente/keepalive.log 2>&1
10
- """
11
- import json
12
- import os
13
- import stat
14
- import subprocess
15
- import sys
16
- from pathlib import Path
17
-
18
- import requests
19
-
20
- LOCAL_COOKIE = Path(
21
- os.environ.get("CONTINENTE_COOKIES_PATH", str(Path.home() / ".continente" / "cookies.json"))
22
- )
23
- PING_URL = "https://www.continente.pt/on/demandware.store/Sites-continente-Site/default/Account-Show"
24
- CHECK_URL = "https://www.continente.pt/conta/encomendas/"
25
- LOG_PREFIX = "[continente-keepalive]"
26
-
27
- USER_AGENT = (
28
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
29
- "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
30
- )
31
-
32
-
33
- def log(msg: str) -> None:
34
- print(f"{LOG_PREFIX} {msg}", flush=True)
35
-
36
-
37
- def load_cookies(path: Path) -> list[dict] | None:
38
- if not path.exists():
39
- return None
40
- try:
41
- data = json.loads(path.read_text())
42
- return data if isinstance(data, list) else data.get("cookies")
43
- except Exception as e:
44
- log(f"Failed to read {path}: {e}")
45
- return None
46
-
47
-
48
- def save_cookies(cookies: list[dict]) -> None:
49
- LOCAL_COOKIE.parent.mkdir(parents=True, exist_ok=True)
50
- LOCAL_COOKIE.write_text(json.dumps(cookies, indent=2, ensure_ascii=False))
51
- os.chmod(LOCAL_COOKIE, stat.S_IRUSR | stat.S_IWUSR)
52
-
53
-
54
- def is_logged_in(session: requests.Session) -> bool:
55
- try:
56
- r = session.get(CHECK_URL, timeout=10, allow_redirects=True)
57
- return "login" not in r.url and r.status_code == 200
58
- except Exception:
59
- return False
60
-
61
-
62
- def refresh_from_browser() -> list[dict] | None:
63
- """Re-run continente-cookie-reader.py to pull fresh cookies from the browser."""
64
- script = Path(__file__).parent / "continente-cookie-reader.py"
65
- if not script.exists():
66
- log("continente-cookie-reader.py not found next to this script.")
67
- return None
68
- try:
69
- result = subprocess.run(
70
- [sys.executable, str(script)],
71
- capture_output=True, text=True, timeout=30
72
- )
73
- output = (result.stdout + result.stderr).strip()
74
- if output:
75
- log(f"Cookie reader: {output}")
76
- return load_cookies(LOCAL_COOKIE)
77
- except Exception as e:
78
- log(f"Cookie reader failed: {e}")
79
- return None
80
-
81
-
82
- def ping(cookies: list[dict]) -> bool:
83
- session = requests.Session()
84
- session.headers.update({
85
- "User-Agent": USER_AGENT,
86
- "Accept-Language": "pt-PT,pt;q=0.9",
87
- })
88
- for c in cookies:
89
- if c.get("name") and c["name"] != "undefined":
90
- session.cookies.set(c["name"], c["value"], domain=c.get("domain", ".continente.pt"))
91
-
92
- logged_in = is_logged_in(session)
93
- if logged_in:
94
- try:
95
- session.get(PING_URL, timeout=10)
96
- except Exception:
97
- pass
98
- return logged_in
99
-
100
-
101
- def main() -> None:
102
- cookies = load_cookies(LOCAL_COOKIE)
103
-
104
- if not cookies:
105
- log("No cookies found — trying to refresh from browser...")
106
- cookies = refresh_from_browser()
107
- if not cookies:
108
- log("ERROR: No cookies found. Run continente-cookie-reader.py first.")
109
- sys.exit(1)
110
-
111
- if ping(cookies):
112
- log("Session alive.")
113
- save_cookies(cookies)
114
- return
115
-
116
- log("Session expired — refreshing from browser...")
117
- cookies = refresh_from_browser()
118
- if not cookies:
119
- log("ERROR: Could not refresh cookies. Run continente-cookie-reader.py manually.")
120
- sys.exit(1)
121
-
122
- if ping(cookies):
123
- save_cookies(cookies)
124
- log("Session restored.")
125
- return
126
-
127
- log(
128
- "ERROR: Still not logged in after refresh. "
129
- "Log into Continente in your browser and re-run continente-cookie-reader.py."
130
- )
131
- sys.exit(1)
132
-
133
-
134
- if __name__ == "__main__":
135
- main()