import os import time import json import requests from datetime import datetime from pathlib import Path # ===== SMS GATEWAY ===== 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/" # Latency target: worst-case <= ~5s POLL_INTERVAL = 1 # seconds # 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(limit=5): r = session.get( RECEIVE_URL, params={ "username": SMS_USER, "password": SMS_PASS, "limit": limit }, timeout=5 ) r.raise_for_status() return r.json() def send_sms(number, message): r = session.get( SEND_URL, params={ "number": number, "message": message, "username": SMS_USER, "password": SMS_PASS }, timeout=5 ) r.raise_for_status() print("๐Ÿ“ค SEND:", r.text) def get_weather(city): r = session.get(f"https://wttr.in/{city}?format=j1", timeout=5) if r.status_code != 200: return f"โŒ Weather unavailable for {city}" data = r.json() weather = data.get("weather", []) if len(weather) < 3: return f"โŒ Weather unavailable for {city}" msg = f"๐ŸŒฆ Weather for {city}:\n" for day in weather[:3]: date = day["date"] avgtemp = day["avgtempC"] desc = day["hourly"][0]["weatherDesc"][0]["value"] weekday = datetime.strptime(date, "%Y-%m-%d").strftime("%a") msg += f"{weekday}: {avgtemp}ยฐC, {desc}\n" return msg.strip() 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(limit=5) inbox = data.get("inbox", []) for sms in inbox: offset = sms.get("offset") if offset is None: continue # 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) print(f"โœ… Replied to {phone}") except Exception as e: print("โš  ERROR:", e) # Keep latency target: no long backoff; next poll in 1s time.sleep(POLL_INTERVAL)