wttr.in/lib/location.py

363 lines
11 KiB
Python
Raw Normal View History

2018-10-07 12:11:59 +02:00
"""
All location related functions and converters.
The main entry point is `location_processing`
which gets `location` and `source_ip_address`
and basing on this information generates
precise location description.
2018-10-07 12:11:59 +02:00
"""
from __future__ import print_function
2018-10-07 12:11:59 +02:00
2020-04-03 20:58:51 +02:00
import sys
import os
2018-10-07 12:11:59 +02:00
import json
import socket
2018-10-07 12:11:59 +02:00
import requests
import geoip2.database
from globals import GEOLITE, GEOLOCATOR_SERVICE, IP2LCACHE, IP2LOCATION_KEY, NOT_FOUND_LOCATION, \
ALIASES, BLACKLIST, IATA_CODES_FILE, IPLOCATION_ORDER, IPINFO_TOKEN
2018-10-07 12:11:59 +02:00
GEOIP_READER = geoip2.database.Reader(GEOLITE)
def ascii_only(string):
"Check if `string` contains only ASCII symbols"
try:
for _ in range(5):
string = string.encode('utf-8')
return True
except UnicodeDecodeError:
return False
def is_ip(ip_addr):
"""
Check if `ip_addr` looks like an IP Address
"""
2020-04-03 20:58:51 +02:00
if sys.version_info[0] < 3:
ip_addr = ip_addr.encode("utf-8")
try:
2020-04-03 20:58:51 +02:00
socket.inet_pton(socket.AF_INET, ip_addr)
return True
except socket.error:
try:
2020-04-03 20:58:51 +02:00
socket.inet_pton(socket.AF_INET6, ip_addr)
return True
except socket.error:
return False
def location_normalize(location):
"""
Normalize location name `location`
"""
#translation_table = dict.fromkeys(map(ord, '!@#$*;'), None)
def _remove_chars(chars, string):
return ''.join(x for x in string if x not in chars)
location = location.lower().replace('_', ' ').replace('+', ' ').strip()
if not location.startswith('moon@'):
location = _remove_chars(r'!@#$*;:\\', location)
return location
2018-10-07 12:11:59 +02:00
def geolocator(location):
"""
Return a GPS pair for specified `location` or None
if nothing can't be found
"""
try:
geo = requests.get('%s/%s' % (GEOLOCATOR_SERVICE, location)).text
except requests.exceptions.ConnectionError as exception:
print("ERROR: %s" % exception)
2018-10-07 12:11:59 +02:00
return None
if geo == "":
return None
try:
answer = json.loads(geo.encode('utf-8'))
return answer
except ValueError as exception:
print("ERROR: %s" % exception)
2018-10-07 12:11:59 +02:00
return None
return None
def ipcachewrite(ip_addr, location):
cached = os.path.join(IP2LCACHE, ip_addr)
if not os.path.exists(IP2LCACHE):
os.makedirs(IP2LCACHE)
with open(cached, 'w') as file:
file.write(location[0] + ';' + location[1])
def ipcache(ip_addr):
2018-10-07 12:11:59 +02:00
cached = os.path.join(IP2LCACHE, ip_addr)
if not os.path.exists(IP2LCACHE):
os.makedirs(IP2LCACHE)
2018-10-26 19:35:13 +02:00
location = None
2018-10-07 12:11:59 +02:00
if os.path.exists(cached):
location = open(cached, 'r').read().split(';')
2020-09-20 11:09:04 +02:00
if len(location) > 3:
return location[3], location[1]
elif len(location) > 1:
return location[0], location[1]
else:
return location[0], None
return None, None
def ip2location(ip_addr):
"Convert IP address `ip_addr` to a location name"
location = ipcache(ip_addr)
if location:
return location
# if IP2LOCATION_KEY is not set, do not the query,
# because the query wont be processed anyway
if IP2LOCATION_KEY:
try:
location = requests\
.get('http://api.ip2location.com/?ip=%s&key=%s&package=WS3' \
% (ip_addr, IP2LOCATION_KEY)).text
except requests.exceptions.ConnectionError:
pass
2018-10-26 19:35:13 +02:00
if location and ';' in location:
2020-09-20 11:09:04 +02:00
ipcachewrite(ip_addr, location)
2020-11-01 20:40:20 +01:00
_, country, region, city = location.split(';')
location = city, region, country
else:
2020-11-01 20:40:20 +01:00
location = location, None, None
2018-10-07 12:11:59 +02:00
2018-10-26 19:35:13 +02:00
return location
2018-10-07 12:11:59 +02:00
def ipinfo(ip_addr):
location = ipcache(ip_addr)
if location:
return location
if IPINFO_TOKEN:
r = requests.get('https://ipinfo.io/%s/json?token=%s' %
(ip_addr, IPINFO_TOKEN))
if r.status_code == 200:
r_json = r.json()
location = r_json["city"], r_json["region"], r_json["country"]
else:
location = None, None, None
if location:
ipcachewrite(ip_addr, location)
return location
def geoip(ip_addr):
location = ipcache(ip_addr)
if location:
return location
2018-11-02 18:12:53 +01:00
try:
response = GEOIP_READER.city(ip_addr)
location = response.city.name, response.subdivisions.name, response.country.name
2018-11-02 18:12:53 +01:00
except geoip2.errors.AddressNotFoundError:
location = None, None, None
if location:
ipcachewrite(ip_addr, location)
return location
2018-10-07 12:11:59 +02:00
def workaround(city, region, country):
# workaround for the strange bug with the country name
# maybe some other countries has this problem too
#
# Having these in a separate function will help if this gets to
# be a problem
if country == 'Russian Federation':
country = 'Russia'
return city, region, country
def get_location(ip_addr):
"""
Return location pair (CITY, COUNTRY) for `ip_addr`
"""
for method in IPLOCATION_ORDER:
if method == 'geoip':
city, country = geoip(ip_addr)
elif method == 'ip2location':
city, country = ip2location(ip_addr)
elif method == 'ipinfo':
city, country = ipinfo(ip_addr)
else:
print("ERROR: invalid iplocation method speficied: %s" % method)
if city is not None:
city, country = workaround(city, country)
return city, country
2018-10-07 12:11:59 +02:00
#
# temporary disabled it because of geoip services capcacity
#
#if city is None and response.location:
# coord = "%s, %s" % (response.location.latitude, response.location.longitude)
# try:
# location = geolocator.reverse(coord, language='en')
# city = location.raw.get('address', {}).get('city')
# except:
# pass
# No methods resulted in a location - return default
return NOT_FOUND_LOCATION, None
2018-10-07 12:11:59 +02:00
def location_canonical_name(location):
"Find canonical name for `location`"
location = location_normalize(location)
2019-02-04 12:12:02 +01:00
if location.lower() in LOCATION_ALIAS:
2018-10-07 12:11:59 +02:00
return LOCATION_ALIAS[location.lower()]
return location
def load_aliases(aliases_filename):
"""
Load aliases from the aliases file
"""
aliases_db = {}
with open(aliases_filename, 'r') as f_aliases:
for line in f_aliases.readlines():
2020-02-12 21:44:56 +01:00
try:
from_, to_ = line.decode('utf-8').split(':', 1)
except AttributeError:
from_, to_ = line.split(':', 1)
2018-10-07 12:11:59 +02:00
aliases_db[location_normalize(from_)] = location_normalize(to_)
return aliases_db
def load_iata_codes(iata_codes_filename):
"""
Load IATA codes from the IATA codes file
"""
with open(iata_codes_filename, 'r') as f_iata_codes:
result = []
for line in f_iata_codes.readlines():
result.append(line.strip())
return set(result)
LOCATION_ALIAS = load_aliases(ALIASES)
LOCATION_BLACK_LIST = [x.strip() for x in open(BLACKLIST, 'r').readlines()]
IATA_CODES = load_iata_codes(IATA_CODES_FILE)
def is_location_blocked(location):
2018-10-07 12:34:36 +02:00
"""
Return True if this location is blocked
or False if it is allowed
"""
2018-10-07 12:11:59 +02:00
return location is not None and location.lower() in LOCATION_BLACK_LIST
def get_hemisphere(location):
"""
Return hemisphere of the location (True = North, False = South).
Assume North and return True if location can't be found.
"""
location_string = location[0]
if location[1] is not None:
location_string += ",%s" % location[1]
geolocation = geolocator(location_string)
if geolocation is None:
return True
return geolocation["latitude"] > 0
2018-10-07 12:34:36 +02:00
def location_processing(location, ip_addr):
"""
"""
2018-10-07 12:11:59 +02:00
2018-10-07 12:34:36 +02:00
# if location is starting with ~
# or has non ascii symbols
# it should be handled like a search term (for geolocator)
override_location_name = None
full_address = None
hide_full_address = False
force_show_full_address = location is not None and location.startswith('~')
2018-10-07 12:34:36 +02:00
# location ~ means that it should be detected automatically,
# and shown in the location line below the report
if location == '~':
location = None
if location and location.lstrip('~ ').startswith('@'):
try:
location, country = get_location(
socket.gethostbyname(
location.lstrip('~ ')[1:]))
location = '~' + location
if country:
location += ", %s" % country
hide_full_address = not force_show_full_address
except:
location, country = NOT_FOUND_LOCATION, None
query_source_location = get_location(ip_addr)
# For moon queries, hemisphere must be found
# True for North, False for South
hemisphere = False
if location is not None and (location.lower()+"@").startswith("moon@"):
hemisphere = get_hemisphere(query_source_location)
country = None
if not location or location == 'MyLocation':
location = ip_addr
if is_ip(location):
location, country = get_location(location)
# here too
if location:
location = '~' + location
if country:
location += ", %s" % country
hide_full_address = not force_show_full_address
if location and not location.startswith('~'):
tmp_location = location_canonical_name(location)
if tmp_location != location:
override_location_name = location
2019-02-04 12:12:02 +01:00
location = tmp_location
# up to this point it is possible that the name
# contains some unicode symbols
# here we resolve them
2019-08-25 20:37:54 +02:00
if location is not None: # and not ascii_only(location):
location = "~" + location.lstrip('~ ')
2019-09-07 15:06:40 +02:00
if not override_location_name:
override_location_name = location.lstrip('~')
2018-10-07 12:34:36 +02:00
2019-08-25 20:37:54 +02:00
# if location is not None and location.upper() in IATA_CODES:
# location = '~%s' % location
2018-10-07 12:34:36 +02:00
2020-04-27 20:40:13 +02:00
if location is not None and not location.startswith("~-,") and location.startswith('~'):
2018-10-07 12:34:36 +02:00
geolocation = geolocator(location_canonical_name(location[1:]))
if geolocation is not None:
2019-09-07 15:06:40 +02:00
if not override_location_name:
override_location_name = location[1:].replace('+', ' ')
2018-10-07 12:34:36 +02:00
location = "%s,%s" % (geolocation['latitude'], geolocation['longitude'])
country = None
if not hide_full_address:
full_address = geolocation['address']
else:
full_address = None
2018-10-07 12:34:36 +02:00
else:
location = NOT_FOUND_LOCATION #location[1:]
2019-09-07 15:06:40 +02:00
2018-10-07 12:34:36 +02:00
return location, \
override_location_name, \
full_address, \
country, \
query_source_location, \
hemisphere