11import math
22from operator import attrgetter
3- from typing import NamedTuple , Optional
3+ from typing import List , NamedTuple , Optional
44
5- from .locality import State , Locality
5+ from .locality import Locality , distance as geo_distance
66
77
8- _AIR_TRAVEL_SPEED = 1000.0 # km/h
9-
10- _EARTH_RADIUS = 6373.0 # km # approximate
8+ _AIR_TRAVEL_SPEED = 277.778 # m/s
119
1210# TODO: Switch to dataclasses when we move to Python3.7+
1311
@@ -16,20 +14,30 @@ class Origin(NamedTuple):
1614 '''A description of a location.
1715 '''
1816
17+ ip : str
1918 city : str
2019 country : str
2120 latitude : float
2221 longitude : float
2322 geopoint : str
2423
2524
25+ class Hop (NamedTuple ):
26+ '''Describes a hop from one location to another that would be
27+ physically impossible in the time between a user's activity in each
28+ location.
29+ '''
30+
31+ origin : Origin
32+ destination : Origin
33+
34+
2635class Alert (NamedTuple ):
2736 '''A container for the data the alerts output by GeoModel contain.
2837 '''
2938
3039 username : str
31- sourceipaddress : str
32- origin : Origin
40+ hops : List [Hop ]
3341
3442
3543def _travel_possible (loc1 : Locality , loc2 : Locality ) -> bool :
@@ -38,53 +46,61 @@ def _travel_possible(loc1: Locality, loc2: Locality) -> bool:
3846 actions took place.
3947 '''
4048
41- lat1 = math .radians (loc1 .latitude )
42- lat2 = math .radians (loc2 .latitude )
43- lon1 = math .radians (loc1 .longitude )
44- lon2 = math .radians (loc2 .longitude )
49+ dist_traveled = 1000 * geo_distance (loc1 , loc2 ) # Convert to metres
4550
46- dlat = lat2 - lat1
47- dlon = lon2 - lon1
48-
49- a = math .sin (dlat / 2.0 ) ** 2 + \
50- math .cos (lat1 ) * math .cos (lat2 ) * math .sin (dlon / 2.0 ) ** 2
51- c = 2 * math .atan2 (math .sqrt (a ), math .sqrt (1 - a ))
52-
53- distance = c * _EARTH_RADIUS
54-
55- seconds_between = (loc2 .lastaction - loc1 .lastaction ).total_seconds ()
56- hours_between = math .ceil (seconds_between / 60.0 / 60.0 )
51+ seconds_between = abs ((loc2 .lastaction - loc1 .lastaction ).total_seconds ())
5752
5853 # We pad the time with an hour to account for things like planes being
5954 # slowed, network delays, etc.
60- return (distance / _AIR_TRAVEL_SPEED ) <= (hours_between - 1 )
55+ ttt = (dist_traveled / _AIR_TRAVEL_SPEED ) # Time to travel the distance.
56+ pad = math .ceil ((1000 * min (loc1 .radius , loc2 .radius )) / _AIR_TRAVEL_SPEED )
57+
58+ return (ttt - pad ) <= seconds_between
6159
6260
63- def alert (user_state : State ) -> Optional [Alert ]:
61+ def alert (
62+ username : str ,
63+ from_evts : List [Locality ],
64+ from_es : List [Locality ]
65+ ) -> Optional [Alert ]:
6466 '''Determine whether an alert should fire given a particular user's
6567 locality state. If an alert should fire, an `Alert` is returned, otherwise
6668 this function returns `None`.
6769 '''
6870
69- locs_to_consider = sorted (user_state .localities , key = attrgetter ('lastaction' ))
71+ relevant_es = sorted (from_es , key = attrgetter ('lastaction' ), reverse = True )[0 :1 ]
72+ all_evts = sorted (from_evts , key = attrgetter ('lastaction' ))
73+ locs_to_consider = relevant_es + all_evts
7074
7175 if len (locs_to_consider ) < 2 :
7276 return None
7377
74- locations = locs_to_consider [- 2 :]
75-
76- if _travel_possible (* locations ):
78+ pairs = [
79+ (locs_to_consider [i ], locs_to_consider [i + 1 ])
80+ for i in range (len (locs_to_consider ) - 1 )
81+ ]
82+
83+ hops = [
84+ Hop (
85+ Origin (
86+ o .sourceipaddress ,
87+ o .city ,
88+ o .country ,
89+ o .latitude ,
90+ o .longitude ,
91+ '{},{}' .format (o .latitude , o .longitude )),
92+ Origin (
93+ d .sourceipaddress ,
94+ d .city ,
95+ d .country ,
96+ d .latitude ,
97+ d .longitude ,
98+ '{},{}' .format (d .latitude , d .longitude )))
99+ for (o , d ) in pairs
100+ if not _travel_possible (o , d )
101+ ]
102+
103+ if len (hops ) == 0 :
77104 return None
78105
79- (ip , city , country , lat , lon ) = (
80- locations [1 ].sourceipaddress ,
81- locations [1 ].city ,
82- locations [1 ].country ,
83- locations [1 ].latitude ,
84- locations [1 ].longitude
85- )
86-
87- geo = '{0},{1}' .format (lat , lon )
88- origin = Origin (city , country , lat , lon , geo )
89-
90- return Alert (user_state .username , ip , origin )
106+ return Alert (username , hops )
0 commit comments