Skip to content

Commit c5ce79d

Browse files
committed
Security: remove hardcoded API key from test_apis.py, add .env.example, fix GPS location to use real CoreLocation + IP geolocation fallback
1 parent 21904f8 commit c5ce79d

File tree

4 files changed

+57
-26
lines changed

4 files changed

+57
-26
lines changed

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copy this file to .env and fill in your keys.
2+
# The .env file is git-ignored and should NEVER be committed.
3+
4+
# Google Gemini API key (https://aistudio.google.com/app/apikey)
5+
GEMINI_API_KEY=your_gemini_api_key_here
6+
7+
# Google Maps Platform API key (https://console.cloud.google.com/google/maps-apis)
8+
# Needs: Directions API, Places API, Geocoding API
9+
GOOGLE_MAPS_API_KEY=your_google_maps_api_key_here

handsfree/executor.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,12 +188,12 @@ def _get_directions(args):
188188
distance = leg["distance"]["text"]
189189
start = leg["start_address"]
190190
end = leg["end_address"]
191-
steps = [
192-
s["html_instructions"].replace("<b>", "").replace("</b>", "")
193-
.replace("<div style=\"font-size:0.9em\">", "")
194-
.replace("</div>", "")
195-
for s in leg["steps"][:5]
196-
]
191+
import re as _re
192+
def _strip_html(h):
193+
h = h.replace("<wbr/>", "").replace("<wbr>", "")
194+
h = h.replace('<div style="font-size:0.9em">', " — ").replace("</div>", "")
195+
return _re.sub(r"<[^>]+>", "", h).strip()
196+
steps = [_strip_html(s["html_instructions"]) for s in leg["steps"][:6]]
197197
maps_url = (
198198
f"https://www.google.com/maps/dir/?api=1"
199199
f"&origin={requests.utils.quote(start)}"
@@ -312,7 +312,7 @@ def _get_current_location(args):
312312
from handsfree.location import get_gps_location
313313
loc = get_gps_location()
314314
if not loc:
315-
raise RuntimeError("GPS unavailable")
315+
raise RuntimeError("Could not determine location — CoreLocation denied and IP lookup failed")
316316

317317
lat, lon = loc["lat"], loc["lon"]
318318

handsfree/location.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,37 +33,42 @@ def is_location_query(text: str) -> bool:
3333
def get_gps_location() -> dict | None:
3434
"""
3535
Retrieve current GPS coordinates using Apple CoreLocation via pyobjc.
36-
Returns dict with lat, lon, address, maps_link — or None if unavailable.
37-
Falls back to a simulated location when running without location permissions.
36+
Falls back to IP-based geolocation if CoreLocation is denied or unavailable.
37+
Returns dict with lat, lon, address, maps_link — or None if all methods fail.
3838
"""
3939
try:
4040
import CoreLocation
4141
import time
4242

4343
manager = CoreLocation.CLLocationManager.alloc().init()
4444

45-
# Request authorization (needed on macOS 10.15+)
4645
auth_status = CoreLocation.CLLocationManager.authorizationStatus()
47-
if auth_status == CoreLocation.kCLAuthorizationStatusNotDetermined:
46+
# kCLAuthorizationStatusDenied = 2, Restricted = 1, NotDetermined = 0
47+
if auth_status in (1, 2):
48+
# Permission denied — skip straight to IP fallback
49+
return _fallback_location()
50+
if auth_status == 0:
4851
manager.requestWhenInUseAuthorization()
49-
time.sleep(1)
52+
time.sleep(1.5)
5053

5154
location = manager.location()
5255
if location is None:
5356
return _fallback_location()
5457

5558
coord = location.coordinate()
5659
lat, lon = coord.latitude, coord.longitude
57-
address = _reverse_geocode(lat, lon)
60+
if lat == 0.0 and lon == 0.0:
61+
return _fallback_location()
5862

63+
address = _reverse_geocode(lat, lon)
5964
return {
6065
"lat": lat,
6166
"lon": lon,
6267
"address": address,
6368
"maps_link": f"https://maps.google.com/?q={lat:.6f},{lon:.6f}",
64-
"source": "CoreLocation (on-device)",
69+
"source": "CoreLocation (on-device GPS)",
6570
}
66-
except Exception as e:
71+
except Exception:
6772
return _fallback_location()
6873

6974

@@ -100,16 +105,31 @@ def completion(placemarks, error):
100105

101106

102107
def _fallback_location() -> dict:
103-
"""Return a plausible simulated location for demo/dev purposes."""
104-
# San Francisco (Civic Center) — good default for the hackathon
105-
lat, lon = 37.7793, -122.4193
106-
return {
107-
"lat": lat,
108-
"lon": lon,
109-
"address": "Civic Center, San Francisco, CA",
110-
"maps_link": f"https://maps.google.com/?q={lat},{lon}",
111-
"source": "simulated (no GPS permission)",
112-
}
108+
"""
109+
Fallback when CoreLocation is unavailable or denied.
110+
Uses IP-based geolocation (ipinfo.io, free, no key needed) for real location.
111+
"""
112+
import requests as _req
113+
try:
114+
resp = _req.get("https://ipinfo.io/json", timeout=4).json()
115+
loc_str = resp.get("loc", "") # "37.7749,-122.4194"
116+
city = resp.get("city", "")
117+
region = resp.get("region", "")
118+
country = resp.get("country", "")
119+
if loc_str and "," in loc_str:
120+
lat, lon = map(float, loc_str.split(","))
121+
address = ", ".join(p for p in [city, region, country] if p)
122+
return {
123+
"lat": lat,
124+
"lon": lon,
125+
"address": address or f"{lat:.4f}, {lon:.4f}",
126+
"maps_link": f"https://maps.google.com/?q={lat:.6f},{lon:.6f}",
127+
"source": "IP geolocation (ipinfo.io)",
128+
}
129+
except Exception:
130+
pass
131+
# Last resort: return None so callers know it truly failed
132+
return None
113133

114134

115135
def inject_location_into_command(text: str, location: dict) -> str:

test_apis.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
sys.path.insert(0, "cactus/python/src")
44
sys.path.insert(0, ".")
55

6-
os.environ.setdefault("GOOGLE_MAPS_API_KEY", "AIzaSyA5IPZCbVqBCbvOK24erpnIShSRltBWgYE")
6+
# Load from environment — set GOOGLE_MAPS_API_KEY in your shell or .env file
7+
if not os.environ.get("GOOGLE_MAPS_API_KEY"):
8+
raise EnvironmentError("GOOGLE_MAPS_API_KEY is not set. See .env.example.")
79

810
from handsfree.executor import execute
911

0 commit comments

Comments
 (0)