Update main.py
This commit is contained in:
139
main.py
139
main.py
@@ -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)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user