Skip to content

Commit

Permalink
Migrate to free ip-api.com geolocation
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jsynowiec committed Jan 10, 2022
1 parent 23707e9 commit 1fe51d9
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 134 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@ coverage
build
out
.vscode/settings.json
keys.json
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ A macOS menu bar application that displays live air quality data from the neares
## Build & installation

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

## Preferences

Expand Down
3 changes: 0 additions & 3 deletions keys.json.example

This file was deleted.

156 changes: 37 additions & 119 deletions src/common/geolocation.ts
Original file line number Diff line number Diff line change
@@ -1,104 +1,43 @@
import { promisify } from 'util';
import { execFile as _execFile } from 'child_process';
import axios from 'axios';
import { omit } from 'lodash';
import getLogger from 'common/logger';

const TIMEOUT = 30 * 1000;

const execFile = promisify(_execFile);

// eslint-disable-next-line @typescript-eslint/no-var-requires
const keys = require('@root/keys.json');

const logger = getLogger('geolocation');

type AirportAccessPoint = {
ssid: string;
macAddress: string;
signalStrength: number;
channel: number;
};

function parseAirportAccessPoints(str: string): AirportAccessPoint[] {
const MAC_RE = /(?:[\da-f]{2}[:]{1}){5}[\da-f]{2}/i;

return str.split('\n').reduce((acc, line) => {
const mac = line.match(MAC_RE);

if (!mac) {
return acc;
}

const macStart = line.indexOf(mac[0]);
const [macAddress, signalStrength, channel] = line
.substr(macStart)
.split(/[ ]+/)
.map((el) => el.trim());

return [
...acc,
{
ssid: line.substr(0, macStart).trim(),
macAddress,
signalStrength: parseInt(signalStrength, 10),
channel: parseInt(channel, 10),
},
];
}, []);
}

async function getwifiAccessPoints(): Promise<AirportAccessPoint[]> {
const { stdout } = await execFile(
'/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport',
['-s'],
{
timeout: TIMEOUT,
},
);
return parseAirportAccessPoints(stdout);
}

type GoogleGeolocationResponse = {
location: {
lat: number;
lng: number;
};
accuracy: number;
};

async function geolocate(wifiAccessPoints): Promise<GoogleGeolocationResponse> {
const response = await axios.post<GoogleGeolocationResponse>(
'https://www.googleapis.com/geolocation/v1/geolocate',
{ wifiAccessPoints },
{
params: {
key: keys.google,
},
},
);

return response.data;
}

export type Location = {
latitude: number;
longitude: number;
};

async function getCurrentPosition(): Promise<GeolocationPosition> {
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
return navigator.geolocation.getCurrentPosition(resolve, reject, { timeout: TIMEOUT });
});
type IPIFYResponse = {
ip: string;
};

type IPAPIResponse = {
status: 'success' | 'fail';
lat: number;
lon: number;
};

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

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

const position = await getCurrentPosition();
const { latitude, longitude } = position.coords;
if (response.data.status != 'success') {
throw new GeolocationError(
GeolocationPositionError.POSITION_UNAVAILABLE,
) as GeolocationPositionError;
}

const { lat: latitude, lon: longitude } = response.data;

return {
latitude,
Expand All @@ -107,39 +46,18 @@ async function getLocationFromNavigator(): Promise<Location> {
}

export async function getLocation(): Promise<Location> {
try {
// geolocate using available WiFi acces points
const wifiAccessPoints = await getwifiAccessPoints();

if (wifiAccessPoints.length > 0) {
const {
location: { lat: latitude, lng: longitude },
accuracy,
} = await geolocate(
wifiAccessPoints.reduce((acc, wifi) => [...acc, omit(wifi, ['ssid'])], []),
);

logger.debug(
`Geolocated using WiFi APs: [${latitude}, ${longitude}], accuracy: ${accuracy}m.`,
);

if (accuracy < 1000) {
logger.debug('Location accuracy is < 1km, returning.');

return {
latitude,
longitude,
} as Location;
}
}
return geolocatePublicIP();
}

// fall back to IP geolocation if accurancy is more than 1km radius
// fall back to IP geolocation if no WiFi access points
return getLocationFromNavigator();
} catch (err) {
logger.warn(err);
class GeolocationError implements GeolocationPositionError {
readonly code: number;
readonly message: string;
readonly PERMISSION_DENIED: number = GeolocationPositionError.PERMISSION_DENIED;
readonly POSITION_UNAVAILABLE: number = GeolocationPositionError.POSITION_UNAVAILABLE;
readonly TIMEOUT: number = GeolocationPositionError.TIMEOUT;

// fall back to IP geolocation on error
return getLocationFromNavigator();
constructor(code: number, message?: string) {
this.code = code;
this.message = message;
}
}
7 changes: 0 additions & 7 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,11 @@ import { Measurements } from 'data/airqmon-api';
import TrayWindowManager from './tray-window-manager';
import PreferencesWindowManager from './preferences-window-manager';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const keys = require('@root/keys.json');

ElectronStore.initRenderer();

let trayWindowManager: TrayWindowManager;
let preferencesWindowManager: PreferencesWindowManager;

if (keys.google) {
process.env.GOOGLE_API_KEY = keys.google;
}

// Don't show the app in the doc
app.dock.hide();

Expand Down

0 comments on commit 1fe51d9

Please sign in to comment.