Update main.py

This commit is contained in:
2026-02-01 15:05:47 +00:00
parent 91df268a57
commit 92a1146025

139
main.py
View File

@@ -1,36 +1,38 @@
import os
import time import time
import json
import requests import requests
from datetime import datetime from datetime import datetime
from pathlib import Path
# ===== SMS GATEWAY ===== # ===== SMS GATEWAY =====
SMS_USER = "weatherbot" SMS_USER = os.getenv("SMS_USER", "weatherbot")
SMS_PASS = "Cupakabra17" SMS_PASS = os.getenv("SMS_PASS", "Cupakabra17")
RECEIVE_URL = "https://sms.internet-master.cz/receive/" RECEIVE_URL = "https://sms.internet-master.cz/receive/"
SEND_URL = "https://sms.internet-master.cz/send/" SEND_URL = "https://sms.internet-master.cz/send/"
POLL_INTERVAL = 1 # 🔥 1 second polling (worst-case 5s) # Latency target: worst-case <= ~5s
MAX_ERRORS_BEFORE_SLEEP = 3 POLL_INTERVAL = 1 # seconds
seen_offsets = set() # Persistence
error_count = 0 STATE_FILE = Path("weatherbot_state.json")
MAX_SEEN_OFFSETS = 5000 # cap memory/state growth
HEADERS = { HEADERS = {
"User-Agent": "weatherbot/1.0 (jakub@jimbuntu)" "User-Agent": "weatherbot/1.0 (jakub@jimbuntu)"
} }
# =======================
session = requests.Session() session = requests.Session()
session.headers.update(HEADERS) session.headers.update(HEADERS)
def fetch_inbox(): def fetch_inbox(limit=5):
r = session.get( r = session.get(
RECEIVE_URL, RECEIVE_URL,
params={ params={
"username": SMS_USER, "username": SMS_USER,
"password": SMS_PASS, "password": SMS_PASS,
"limit": 5 "limit": limit
}, },
timeout=5 timeout=5
) )
@@ -64,7 +66,6 @@ def get_weather(city):
return f"❌ Weather unavailable for {city}" return f"❌ Weather unavailable for {city}"
msg = f"🌦 Weather for {city}:\n" msg = f"🌦 Weather for {city}:\n"
for day in weather[:3]: for day in weather[:3]:
date = day["date"] date = day["date"]
avgtemp = day["avgtempC"] avgtemp = day["avgtempC"]
@@ -74,40 +75,120 @@ def get_weather(city):
return msg.strip() return msg.strip()
# ===== MAIN LOOP ===== def load_state():
print("📡 SMS Weather Bot started (≤5s reply mode)") 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: while True:
try: try:
data = fetch_inbox() data = fetch_inbox(limit=5)
inbox = data.get("inbox", []) inbox = data.get("inbox", [])
error_count = 0 # reset on success
for sms in inbox: for sms in inbox:
offset = sms["offset"] offset = sms.get("offset")
if offset in seen_offsets: if offset is None:
continue continue
phone = sms["phone"] # If already processed, skip
text = sms["message"].strip() 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}") print(f"📨 {phone}: {text}")
reply = get_weather(text) reply = get_weather(text)
send_sms(phone, reply) send_sms(phone, reply)
seen_offsets.add(offset)
print(f"✅ Replied to {phone}") print(f"✅ Replied to {phone}")
except Exception as e: except Exception as e:
error_count += 1
print("⚠ ERROR:", e) print("⚠ ERROR:", e)
# Keep latency target: no long backoff; next poll in 1s
# If gateway is unstable, slow down briefly time.sleep(POLL_INTERVAL)
if error_count >= MAX_ERRORS_BEFORE_SLEEP:
print("⏳ Too many errors, sleeping 5s")
time.sleep(5)
error_count = 0
time.sleep(POLL_INTERVAL)