Skip to content

Commit 1ef1056

Browse files
committed
fixed location handling
Fixed Location Handling: Removed the non-existent requestLocation() call Better handling of Fly.io headers to get client IP Added two different IP geolocation services as fallbacks (ipapi.co and ip-api.com) and better voice handling
1 parent 4bbfb42 commit 1ef1056

File tree

1 file changed

+115
-107
lines changed

1 file changed

+115
-107
lines changed

src/index.ts

Lines changed: 115 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// Version: 1.0.1
2-
// Description: Air Quality Augmentos App
1+
// Version: 1.2.1
2+
// Description: Air Quality Augmentos App - Fixed Location Handling
33
import 'dotenv/config';
44
import express from 'express';
55
import path from 'path';
@@ -13,7 +13,7 @@ const packageJson = JSON.parse(
1313
readFileSync(path.join(__dirname, '../package.json'), 'utf-8')
1414
);
1515
const APP_VERSION = packageJson.version;
16-
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
16+
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
1717
const PACKAGE_NAME = process.env.PACKAGE_NAME || 'com.everywoah.airquality';
1818
const AUGMENTOS_API_KEY = process.env.AUGMENTOS_API_KEY;
1919
const AQI_TOKEN = process.env.AQI_TOKEN;
@@ -46,18 +46,19 @@ class AirQualityApp extends TpaServer {
4646
private activeSessions = new Map<string, {
4747
userId: string;
4848
started: Date;
49-
locationAttempted: boolean;
5049
locationObtained: boolean;
50+
lastLocation?: { lat: number; lon: number };
5151
}>();
52-
private requestCount = 0;
5352

5453
private readonly VOICE_COMMANDS = [
5554
"air quality",
5655
"what's the air like",
5756
"pollution",
5857
"how clean is the air",
5958
"is the air safe",
60-
"nearest air quality station"
59+
"nearest air quality station",
60+
"air quality here",
61+
"air pollution here"
6162
];
6263

6364
constructor() {
@@ -75,10 +76,7 @@ class AirQualityApp extends TpaServer {
7576

7677
// Middleware
7778
app.use((req, res, next) => {
78-
this.requestCount++;
79-
const requestId = crypto.randomUUID();
80-
res.set('X-Request-ID', requestId);
81-
console.log(`[${new Date().toISOString()}] REQ#${this.requestCount} ${req.method} ${req.path}`);
79+
res.set('X-Request-ID', crypto.randomUUID());
8280
next();
8381
});
8482
app.use(express.json());
@@ -118,7 +116,6 @@ class AirQualityApp extends TpaServer {
118116
userId: req.body.userId,
119117
packageName: PACKAGE_NAME
120118
});
121-
console.log(`Session initialized: ${req.body.sessionId} for user ${req.body.userId}`);
122119
res.json({ status: 'success' });
123120
} catch (error) {
124121
console.error('Session init failed:', error);
@@ -134,100 +131,89 @@ class AirQualityApp extends TpaServer {
134131
this.activeSessions.set(sessionId, {
135132
userId,
136133
started: new Date(),
137-
locationAttempted: false,
138134
locationObtained: false
139135
});
140136

141137
console.log(`New session ${sessionId} started for user ${userId}`);
142-
143-
// Request location immediately
144-
try {
145-
const sessionData = this.activeSessions.get(sessionId);
146-
if (sessionData) {
147-
sessionData.locationAttempted = true;
148-
}
149-
await session.requestLocation();
150-
console.log(`Location requested for session ${sessionId}`);
151-
} catch (error) {
152-
console.error(`Failed to request location for session ${sessionId}:`, error);
153-
}
154138

155139
// 🔍 PRIORITY: SDK location callback
156140
session.events.onLocation(async (coords) => {
157141
console.log(`📍 Received coordinates from SDK: ${coords.lat}, ${coords.lon}`);
158142
const sessionData = this.activeSessions.get(sessionId);
159143
if (sessionData) {
160144
sessionData.locationObtained = true;
145+
sessionData.lastLocation = { lat: coords.lat, lon: coords.lon };
161146
}
162147
await this.showAirQuality(session, coords.lat, coords.lon, false);
163148
});
164149

165150
// 🎤 Voice command trigger
166-
session.onTranscriptionForLanguage('en-US', (transcript) => {
151+
session.onTranscriptionForLanguage('en-US', async (transcript) => {
167152
const text = transcript.text.toLowerCase();
168153
console.log(`🎤 Heard: "${text}" for session ${sessionId}`);
154+
169155
if (this.VOICE_COMMANDS.some(cmd => text.includes(cmd.toLowerCase()))) {
170-
this.handleAirQualityRequest(session, sessionId).catch(console.error);
156+
const sessionData = this.activeSessions.get(sessionId);
157+
if (sessionData?.lastLocation) {
158+
// Use last known location if available
159+
await this.showAirQuality(
160+
session,
161+
sessionData.lastLocation.lat,
162+
sessionData.lastLocation.lon,
163+
false
164+
);
165+
} else {
166+
// Try to get fresh location
167+
await this.handleAirQualityRequest(session, sessionId);
168+
}
171169
}
172170
});
173171

174-
// Fallback: Check if session has location after a delay
175-
setTimeout(async () => {
176-
await this.handleAirQualityRequest(session, sessionId);
177-
}, 2000);
172+
// Initial location attempt
173+
setTimeout(() => {
174+
this.handleAirQualityRequest(session, sessionId).catch(console.error);
175+
}, 1000);
178176
}
179177

180178
private async handleAirQualityRequest(session: TpaSession, sessionId: string): Promise<void> {
181179
const sessionData = this.activeSessions.get(sessionId);
182180
if (!sessionData) return;
183181

184-
// If we already have location from SDK callback, skip
185-
if (sessionData.locationObtained) return;
186-
187-
// Check if we have session location
182+
// 1. First try session.location if available
188183
if (session.location?.latitude && session.location?.longitude) {
189-
console.log(`📍 Using session.location for ${sessionId}: ${session.location.latitude}, ${session.location.longitude}`);
184+
console.log(`📍 Using session.location: ${session.location.latitude}, ${session.location.longitude}`);
190185
sessionData.locationObtained = true;
191-
await this.showAirQuality(session, session.location.latitude, session.location.longitude, false);
186+
sessionData.lastLocation = {
187+
lat: session.location.latitude,
188+
lon: session.location.longitude
189+
};
190+
await this.showAirQuality(
191+
session,
192+
session.location.latitude,
193+
session.location.longitude,
194+
false
195+
);
192196
return;
193197
}
194198

195-
// Try IP-based location
196-
try {
197-
const ipLocation = await this.getIpBasedLocation(session);
198-
console.log(`📍 Using IP-based location for ${sessionId}: ${ipLocation.lat}, ${ipLocation.lon}`);
199-
await this.showAirQuality(session, ipLocation.lat, ipLocation.lon, false);
200-
sessionData.locationObtained = true;
201-
} catch (error) {
202-
console.error(`IP/header geolocation failed for ${sessionId}:`, error);
203-
// Final fallback to London with warning
204-
console.log(`📍 Using default location for ${sessionId}`);
205-
await this.showAirQuality(session, 51.5074, -0.1278, true);
206-
}
207-
}
208-
209-
private async getNearestAQIStation(lat: number, lon: number): Promise<AQIStationData> {
199+
// 2. Try client IP geolocation (respecting Fly.io headers)
210200
try {
211-
console.log(`Fetching AQI data for coordinates: ${lat}, ${lon}`);
212-
const response = await axios.get(
213-
`https://api.waqi.info/feed/geo:${lat};${lon}/?token=${AQI_TOKEN}`,
214-
{ timeout: 5000 }
215-
);
216-
if (response.data.status !== 'ok') {
217-
throw new Error(response.data.data || 'Station data unavailable');
201+
const clientIp = this.getClientIp(session);
202+
if (clientIp) {
203+
const ipLocation = await this.getIpLocation(clientIp);
204+
console.log(`📍 Using client IP location: ${ipLocation.lat}, ${ipLocation.lon}`);
205+
sessionData.locationObtained = true;
206+
sessionData.lastLocation = ipLocation;
207+
await this.showAirQuality(session, ipLocation.lat, ipLocation.lon, false);
208+
return;
218209
}
219-
console.log(`AQI data received: ${response.data.data.aqi} from ${response.data.data.city?.name || 'Unknown station'}`);
220-
return {
221-
aqi: response.data.data.aqi,
222-
station: {
223-
name: response.data.data.city?.name || 'Nearest AQI station',
224-
geo: response.data.data.city?.geo || [lat, lon]
225-
}
226-
};
227210
} catch (error) {
228-
console.error('AQI station fetch failed:', error);
229-
throw error;
211+
console.error('Client IP geolocation failed:', error);
230212
}
213+
214+
// 3. Final fallback with warning
215+
console.log('⚠️ Using default London location');
216+
await this.showAirQuality(session, 51.5074, -0.1278, true);
231217
}
232218

233219
private async showAirQuality(session: TpaSession, lat: number, lon: number, isFallback: boolean): Promise<void> {
@@ -237,7 +223,7 @@ class AirQualityApp extends TpaServer {
237223

238224
let locationMessage = `📍 ${station.station.name}`;
239225
if (isFallback) {
240-
locationMessage += "\n⚠️ Using default location (couldn't detect yours)";
226+
locationMessage = `⚠️ ${station.station.name} (default location - enable GPS for accurate results)`;
241227
}
242228

243229
await session.layouts.showTextWall(
@@ -256,52 +242,74 @@ class AirQualityApp extends TpaServer {
256242
}
257243
}
258244

259-
private async getIpBasedLocation(session: TpaSession): Promise<{ lat: number, lon: number }> {
245+
private getClientIp(session: TpaSession): string | null {
246+
if (!session.request?.headers) return null;
247+
248+
const headers = session.request.headers;
249+
250+
// Fly.io headers take priority
251+
if (headers['fly-client-ip']) {
252+
return headers['fly-client-ip'] as string;
253+
}
254+
255+
// Standard headers
256+
const xForwardedFor = headers['x-forwarded-for'];
257+
if (xForwardedFor) {
258+
return (Array.isArray(xForwardedFor) ? xForwardedFor[0] : xForwardedFor).split(',')[0].trim();
259+
}
260+
261+
return headers['x-real-ip'] as string || null;
262+
}
263+
264+
private async getIpLocation(ip: string): Promise<{ lat: number; lon: number }> {
260265
try {
261-
// Check for Fly.io geolocation headers first
262-
if (session.request?.headers) {
263-
const headers = session.request.headers;
264-
265-
// Fly.io specific geo headers
266-
if (headers['fly-geo-lat'] && headers['fly-geo-long']) {
267-
const lat = parseFloat(headers['fly-geo-lat']);
268-
const lon = parseFloat(headers['fly-geo-long']);
269-
console.log(`Using Fly.io geo headers: ${lat}, ${lon}`);
270-
return { lat, lon };
271-
}
272-
273-
// Get client IP from headers
274-
const clientIp = headers['fly-client-ip'] ||
275-
headers['x-forwarded-for']?.split(',')[0] ||
276-
headers['x-real-ip'];
277-
278-
if (clientIp && !['127.0.0.1', 'localhost'].includes(clientIp)) {
279-
console.log(`Attempting IP geolocation for: ${clientIp}`);
280-
const ipLocation = await axios.get(`https://ipapi.co/${clientIp}/json/`, { timeout: 3000 });
281-
if (ipLocation.data.latitude && ipLocation.data.longitude) {
282-
return {
283-
lat: ipLocation.data.latitude,
284-
lon: ipLocation.data.longitude
285-
};
286-
}
287-
}
266+
// First try ipapi.co
267+
const response = await axios.get(`https://ipapi.co/${ip}/json/`, { timeout: 3000 });
268+
if (response.data.latitude && response.data.longitude) {
269+
return {
270+
lat: response.data.latitude,
271+
lon: response.data.longitude
272+
};
288273
}
289274

290-
// Last resort: server IP location
291-
console.log(`Falling back to server IP geolocation`);
292-
const serverIp = await axios.get('https://ipapi.co/json/', { timeout: 3000 });
293-
if (serverIp.data.latitude && serverIp.data.longitude) {
294-
return {
295-
lat: serverIp.data.latitude,
296-
lon: serverIp.data.longitude
275+
// Fallback to ip-api.com if ipapi fails
276+
const fallbackResponse = await axios.get(`http://ip-api.com/json/${ip}`, { timeout: 3000 });
277+
if (fallbackResponse.data.lat && fallbackResponse.data.lon) {
278+
return {
279+
lat: fallbackResponse.data.lat,
280+
lon: fallbackResponse.data.lon
297281
};
298282
}
283+
284+
throw new Error('No location data from geolocation services');
299285
} catch (error) {
300-
console.warn("IP geolocation failed:", error);
286+
console.error('IP geolocation failed:', error);
287+
throw error;
288+
}
289+
}
290+
291+
private async getNearestAQIStation(lat: number, lon: number): Promise<AQIStationData> {
292+
try {
293+
const response = await axios.get(
294+
`https://api.waqi.info/feed/geo:${lat};${lon}/?token=${AQI_TOKEN}`,
295+
{ timeout: 5000 }
296+
);
297+
298+
if (response.data.status !== 'ok') {
299+
throw new Error(response.data.data || 'Station data unavailable');
300+
}
301+
302+
return {
303+
aqi: response.data.data.aqi,
304+
station: {
305+
name: response.data.data.city?.name || 'Nearest AQI station',
306+
geo: response.data.data.city?.geo || [lat, lon]
307+
}
308+
};
309+
} catch (error) {
310+
console.error('AQI station fetch failed:', error);
301311
throw error;
302312
}
303-
304-
throw new Error("All geolocation methods failed");
305313
}
306314
}
307315

0 commit comments

Comments
 (0)