diff --git a/main.py b/main.py index f9a12b7..2d9e8dd 100644 --- a/main.py +++ b/main.py @@ -1,36 +1,38 @@ +import os import time +import json import requests from datetime import datetime +from pathlib import Path # ===== SMS GATEWAY ===== -SMS_USER = "weatherbot" -SMS_PASS = "Cupakabra17" +SMS_USER = os.getenv("SMS_USER", "weatherbot") +SMS_PASS = os.getenv("SMS_PASS", "Cupakabra17") RECEIVE_URL = "https://sms.internet-master.cz/receive/" SEND_URL = "https://sms.internet-master.cz/send/" -POLL_INTERVAL = 1 # ๐Ÿ”ฅ 1 second polling (worst-case โ‰ค5s) -MAX_ERRORS_BEFORE_SLEEP = 3 +# Latency target: worst-case <= ~5s +POLL_INTERVAL = 1 # seconds -seen_offsets = set() -error_count = 0 +# Persistence +STATE_FILE = Path("weatherbot_state.json") +MAX_SEEN_OFFSETS = 5000 # cap memory/state growth HEADERS = { "User-Agent": "weatherbot/1.0 (jakub@jimbuntu)" } -# ======================= - session = requests.Session() session.headers.update(HEADERS) -def fetch_inbox(): +def fetch_inbox(limit=5): r = session.get( RECEIVE_URL, params={ "username": SMS_USER, "password": SMS_PASS, - "limit": 5 + "limit": limit }, timeout=5 ) @@ -64,7 +66,6 @@ def get_weather(city): return f"โŒ Weather unavailable for {city}" msg = f"๐ŸŒฆ Weather for {city}:\n" - for day in weather[:3]: date = day["date"] avgtemp = day["avgtempC"] @@ -74,40 +75,120 @@ def get_weather(city): return msg.strip() -# ===== MAIN LOOP ===== -print("๐Ÿ“ก SMS Weather Bot started (โ‰ค5s reply mode)") +def load_state(): + if not STATE_FILE.exists(): + return {"seen_offsets": []} + try: + with STATE_FILE.open("r", encoding="utf-8") as f: + data = json.load(f) + if not isinstance(data, dict): + return {"seen_offsets": []} + if "seen_offsets" not in data or not isinstance(data["seen_offsets"], list): + data["seen_offsets"] = [] + return data + except Exception: + # If state is corrupt, start clean (but startup priming still prevents backlog replies) + return {"seen_offsets": []} + +def save_state(state): + # Keep only the newest N offsets (as ints if possible) + seen = state.get("seen_offsets", []) + if len(seen) > MAX_SEEN_OFFSETS: + seen = seen[-MAX_SEEN_OFFSETS:] + state["seen_offsets"] = seen + + tmp = STATE_FILE.with_suffix(".json.tmp") + with tmp.open("w", encoding="utf-8") as f: + json.dump(state, f, ensure_ascii=False) + tmp.replace(STATE_FILE) + +def prime_seen_offsets(seen_set): + """ + On startup, mark whatever is currently in the inbox as 'already seen' + so we do NOT reply to backlog after a restart. + """ + try: + data = fetch_inbox(limit=20) + inbox = data.get("inbox", []) + primed = 0 + for sms in inbox: + offset = sms.get("offset") + if offset is None: + continue + if offset not in seen_set: + seen_set.add(offset) + primed += 1 + if primed: + print(f"๐Ÿงน Primed {primed} existing message(s) as already-seen (no backlog replies).") + else: + print("๐Ÿงน No existing messages to prime.") + except Exception as e: + print("โš  Prime failed (continuing):", e) + +def add_seen(seen_set, seen_list, offset): + # offset can be int/str depending on API; store exactly as returned + if offset in seen_set: + return False + seen_set.add(offset) + seen_list.append(offset) + # cap list size + if len(seen_list) > MAX_SEEN_OFFSETS: + # drop oldest chunk + drop = len(seen_list) - MAX_SEEN_OFFSETS + for old in seen_list[:drop]: + seen_set.discard(old) + del seen_list[:drop] + return True + +# ===== MAIN ===== +print("๐Ÿ“ก SMS Weather Bot started (โ‰ค5s reply + persistent offsets)") + +state = load_state() +seen_list = state.get("seen_offsets", []) +seen_set = set(seen_list) + +# Prevent replying to old messages sitting in inbox after restart +prime_seen_offsets(seen_set) +# Sync list to set after priming (preserve order by appending primed ones) +# (We don't know which were primed vs existing; easiest is to rewrite list from set as a stable list) +# But to preserve bounded size, we just extend list with items missing: +for off in list(seen_set): + if off not in seen_list: + seen_list.append(off) +state["seen_offsets"] = seen_list +save_state(state) while True: try: - data = fetch_inbox() + data = fetch_inbox(limit=5) inbox = data.get("inbox", []) - error_count = 0 # reset on success - for sms in inbox: - offset = sms["offset"] - if offset in seen_offsets: + offset = sms.get("offset") + if offset is None: continue - phone = sms["phone"] - text = sms["message"].strip() + # If already processed, skip + if offset in seen_set: + continue + + phone = sms.get("phone", "") + text = (sms.get("message") or "").strip() + + # Mark seen immediately to avoid duplicate replies if send fails mid-loop + add_seen(seen_set, seen_list, offset) + state["seen_offsets"] = seen_list + save_state(state) print(f"๐Ÿ“จ {phone}: {text}") reply = get_weather(text) send_sms(phone, reply) - seen_offsets.add(offset) print(f"โœ… Replied to {phone}") except Exception as e: - error_count += 1 print("โš  ERROR:", e) + # Keep latency target: no long backoff; next poll in 1s - # If gateway is unstable, slow down briefly - if error_count >= MAX_ERRORS_BEFORE_SLEEP: - print("โณ Too many errors, sleeping 5s") - time.sleep(5) - error_count = 0 - - time.sleep(POLL_INTERVAL) \ No newline at end of file + time.sleep(POLL_INTERVAL)