Skip to content

Commit 1fe51d9

Browse files
committed
Migrate to free ip-api.com geolocation
Mitigates #50 through replacing the Google Geolocation API with a free ip-api.com. The acquired location will have a lower accuracy because the SSID-based geolocation is now removed.
1 parent 23707e9 commit 1fe51d9

File tree

5 files changed

+40
-134
lines changed

5 files changed

+40
-134
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,3 @@ coverage
55
build
66
out
77
.vscode/settings.json
8-
keys.json

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,9 @@ A macOS menu bar application that displays live air quality data from the neares
2525
## Build & installation
2626

2727
1. Clone the [latest release][airqmon-latest-release].
28-
2. Provide your own Google Geolocation API key in the `keys.json` file.
29-
3. Install the dependancies with `yarn install`.
30-
4. Build the binary with `yarn run package`.
31-
5. Drag the binary to your `Applications` folder.
28+
2. Install the dependancies with `yarn install`.
29+
3. Build the binary with `yarn run package`.
30+
4. Drag the binary to your `Applications` folder.
3231

3332
## Preferences
3433

keys.json.example

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/common/geolocation.ts

Lines changed: 37 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,43 @@
1-
import { promisify } from 'util';
2-
import { execFile as _execFile } from 'child_process';
31
import axios from 'axios';
4-
import { omit } from 'lodash';
52
import getLogger from 'common/logger';
63

7-
const TIMEOUT = 30 * 1000;
8-
9-
const execFile = promisify(_execFile);
10-
11-
// eslint-disable-next-line @typescript-eslint/no-var-requires
12-
const keys = require('@root/keys.json');
13-
144
const logger = getLogger('geolocation');
155

16-
type AirportAccessPoint = {
17-
ssid: string;
18-
macAddress: string;
19-
signalStrength: number;
20-
channel: number;
21-
};
22-
23-
function parseAirportAccessPoints(str: string): AirportAccessPoint[] {
24-
const MAC_RE = /(?:[\da-f]{2}[:]{1}){5}[\da-f]{2}/i;
25-
26-
return str.split('\n').reduce((acc, line) => {
27-
const mac = line.match(MAC_RE);
28-
29-
if (!mac) {
30-
return acc;
31-
}
32-
33-
const macStart = line.indexOf(mac[0]);
34-
const [macAddress, signalStrength, channel] = line
35-
.substr(macStart)
36-
.split(/[ ]+/)
37-
.map((el) => el.trim());
38-
39-
return [
40-
...acc,
41-
{
42-
ssid: line.substr(0, macStart).trim(),
43-
macAddress,
44-
signalStrength: parseInt(signalStrength, 10),
45-
channel: parseInt(channel, 10),
46-
},
47-
];
48-
}, []);
49-
}
50-
51-
async function getwifiAccessPoints(): Promise<AirportAccessPoint[]> {
52-
const { stdout } = await execFile(
53-
'/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport',
54-
['-s'],
55-
{
56-
timeout: TIMEOUT,
57-
},
58-
);
59-
return parseAirportAccessPoints(stdout);
60-
}
61-
62-
type GoogleGeolocationResponse = {
63-
location: {
64-
lat: number;
65-
lng: number;
66-
};
67-
accuracy: number;
68-
};
69-
70-
async function geolocate(wifiAccessPoints): Promise<GoogleGeolocationResponse> {
71-
const response = await axios.post<GoogleGeolocationResponse>(
72-
'https://www.googleapis.com/geolocation/v1/geolocate',
73-
{ wifiAccessPoints },
74-
{
75-
params: {
76-
key: keys.google,
77-
},
78-
},
79-
);
80-
81-
return response.data;
82-
}
83-
846
export type Location = {
857
latitude: number;
868
longitude: number;
879
};
8810

89-
async function getCurrentPosition(): Promise<GeolocationPosition> {
90-
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
91-
return navigator.geolocation.getCurrentPosition(resolve, reject, { timeout: TIMEOUT });
92-
});
11+
type IPIFYResponse = {
12+
ip: string;
13+
};
14+
15+
type IPAPIResponse = {
16+
status: 'success' | 'fail';
17+
lat: number;
18+
lon: number;
19+
};
9320

94-
return position;
21+
async function getPublicIP(): Promise<string> {
22+
logger.debug('Obtaining public IP address.');
23+
const response = await axios.get<IPIFYResponse>('https://api.ipify.org?format=json');
24+
return response.data.ip;
9525
}
9626

97-
async function getLocationFromNavigator(): Promise<Location> {
98-
logger.debug('Using the default IP-based geolocation.');
27+
async function geolocatePublicIP(): Promise<Location> {
28+
const ip = await getPublicIP();
29+
logger.debug(`Using the ip-api geolocation with public IP address, ${ip}.`);
30+
const response = await axios.post<IPAPIResponse>(
31+
`http://ip-api.com/json/${ip}?fields=status,lat,lon`,
32+
);
9933

100-
const position = await getCurrentPosition();
101-
const { latitude, longitude } = position.coords;
34+
if (response.data.status != 'success') {
35+
throw new GeolocationError(
36+
GeolocationPositionError.POSITION_UNAVAILABLE,
37+
) as GeolocationPositionError;
38+
}
39+
40+
const { lat: latitude, lon: longitude } = response.data;
10241

10342
return {
10443
latitude,
@@ -107,39 +46,18 @@ async function getLocationFromNavigator(): Promise<Location> {
10746
}
10847

10948
export async function getLocation(): Promise<Location> {
110-
try {
111-
// geolocate using available WiFi acces points
112-
const wifiAccessPoints = await getwifiAccessPoints();
113-
114-
if (wifiAccessPoints.length > 0) {
115-
const {
116-
location: { lat: latitude, lng: longitude },
117-
accuracy,
118-
} = await geolocate(
119-
wifiAccessPoints.reduce((acc, wifi) => [...acc, omit(wifi, ['ssid'])], []),
120-
);
121-
122-
logger.debug(
123-
`Geolocated using WiFi APs: [${latitude}, ${longitude}], accuracy: ${accuracy}m.`,
124-
);
125-
126-
if (accuracy < 1000) {
127-
logger.debug('Location accuracy is < 1km, returning.');
128-
129-
return {
130-
latitude,
131-
longitude,
132-
} as Location;
133-
}
134-
}
49+
return geolocatePublicIP();
50+
}
13551

136-
// fall back to IP geolocation if accurancy is more than 1km radius
137-
// fall back to IP geolocation if no WiFi access points
138-
return getLocationFromNavigator();
139-
} catch (err) {
140-
logger.warn(err);
52+
class GeolocationError implements GeolocationPositionError {
53+
readonly code: number;
54+
readonly message: string;
55+
readonly PERMISSION_DENIED: number = GeolocationPositionError.PERMISSION_DENIED;
56+
readonly POSITION_UNAVAILABLE: number = GeolocationPositionError.POSITION_UNAVAILABLE;
57+
readonly TIMEOUT: number = GeolocationPositionError.TIMEOUT;
14158

142-
// fall back to IP geolocation on error
143-
return getLocationFromNavigator();
59+
constructor(code: number, message?: string) {
60+
this.code = code;
61+
this.message = message;
14462
}
14563
}

src/main.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,11 @@ import { Measurements } from 'data/airqmon-api';
99
import TrayWindowManager from './tray-window-manager';
1010
import PreferencesWindowManager from './preferences-window-manager';
1111

12-
// eslint-disable-next-line @typescript-eslint/no-var-requires
13-
const keys = require('@root/keys.json');
14-
1512
ElectronStore.initRenderer();
1613

1714
let trayWindowManager: TrayWindowManager;
1815
let preferencesWindowManager: PreferencesWindowManager;
1916

20-
if (keys.google) {
21-
process.env.GOOGLE_API_KEY = keys.google;
22-
}
23-
2417
// Don't show the app in the doc
2518
app.dock.hide();
2619

0 commit comments

Comments
 (0)