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
33import 'dotenv/config' ;
44import express from 'express' ;
55import path from 'path' ;
@@ -13,7 +13,7 @@ const packageJson = JSON.parse(
1313 readFileSync ( path . join ( __dirname , '../package.json' ) , 'utf-8' )
1414) ;
1515const 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 ;
1717const PACKAGE_NAME = process . env . PACKAGE_NAME || 'com.everywoah.airquality' ;
1818const AUGMENTOS_API_KEY = process . env . AUGMENTOS_API_KEY ;
1919const 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