diff --git a/contrib_bots/lib/CommuteBot/doc.md b/contrib_bots/lib/CommuteBot/doc.md new file mode 100644 index 0000000000..48e969f458 --- /dev/null +++ b/contrib_bots/lib/CommuteBot/doc.md @@ -0,0 +1,54 @@ +This bot will allow briefings of estimated travel times, distances and fare information for transit travel. + +It can respond to: departure times, arrival times, user preferences (toll avoidance, highway avoidance) and a mode of transport + +It can output: fare information, more detailed addresses on origin and destination, duration in traffic information, metric and imperical units and information in various languages. + +The bot will respond to the same stream input was in. And if called as private message, the bot will reply with a private message. + +To setup the bot, you will first need to move google-commute.ini into the user home directory and add an API key. + +Move
~/zulip/contrib_bots/lib/CommuteBot/google-commute.ini
into
~/google-commute.ini
+ +To add an API key, please visit: https://developers.google.com/maps/documentation/distance-matrix/start to retrieve a key and copy your api key into google-commute.ini + +Sample input and output: + +
@commute help
+ +
Obligatory Inputs:
+    Origin e.g. origins=New+York,NY,USA
+    Destination e.g. destinations=Chicago,IL,USA
+
+Optional Inputs:
+    Mode Of Transport (inputs: driving, walking, bicycling, transit)
+    Default mode (no mode input) is driving
+    e.g. mode=driving or mode=transit
+    Units (metric or imperial)
+    e.g. units=metric
+    Restrictions (inputs: tolls, highways, ferries, indoor)
+    e.g. avoid=tolls
+    Departure Time (inputs: now or (YYYY, MM, DD, HH, MM, SS) departing)
+    e.g. departure_time=now or departure_time=2016,12,17,23,40,40
+    Arrival Time (inputs: (YYYY, MM, DD, HH, MM, SS) arriving)
+    e.g. arrival_time=2016,12,17,23,40,40
+    Languages:
+    Languages list: https://developers.google.com/maps/faq#languagesupport)
+    e.g. language=fr
+
+ +Sample request: +

+    @commute origins=Chicago,IL,USA destinations=New+York,NY,USA language=fr
+    
+ +Please note: + Fare information can be derived, though is solely dependent on the + availability of the information released by public transport operators. + Duration in traffic can only be derived if a departure time is set. + If a location has spaces in its name, please use a + symbol in the + place of the space/s. + A departure time and a arrival time can not be inputted at the same time + No spaces within addresses. + Departure times and arrival times must be in the UTC timezone, + you can use the timezone bot. diff --git a/contrib_bots/lib/CommuteBot/google-commute.ini b/contrib_bots/lib/CommuteBot/google-commute.ini new file mode 100644 index 0000000000..88f60d4977 --- /dev/null +++ b/contrib_bots/lib/CommuteBot/google-commute.ini @@ -0,0 +1,2 @@ +[Google.com] +api_key = abcdefghijklmnopqrstuvwxyz diff --git a/contrib_bots/lib/commute_bot.py b/contrib_bots/lib/commute_bot.py new file mode 100644 index 0000000000..8c772163cf --- /dev/null +++ b/contrib_bots/lib/commute_bot.py @@ -0,0 +1,263 @@ +from __future__ import print_function + +import datetime as dt +import requests +from os.path import expanduser +from six.moves import configparser as cp + +home = expanduser('~') +CONFIG_PATH = home + '/google-commute.ini' + +class CommuteHandler(object): + ''' + This plugin provides information regarding commuting + from an origin to a destination, providing a multitude of information. + It looks for messages starting with '@commute'. + ''' + + def __init__(self): + self.api_key = self.get_api_key() + + def usage(self): + return ''' + This plugin will allow briefings of estimated travel times, + distances and fare information for transit travel. + It can vary outputs depending on traffic conditions, departure and + arrival times as well as user preferences + (toll avoidance, preference for bus travel, etc.). + It looks for messages starting with '@commute'. + + Users should input an origin and a destination + in any stream or through private messages to the bot to receive a + response in the same stream or through private messages if the + input was originally private. + + Sample input: + @commute origins=Chicago,IL,USA destinations=New+York,NY,USA + @commute help + ''' + + help_info = ''' + Obligatory Inputs: + Origin e.g. origins=New+York,NY,USA + Destination e.g. destinations=Chicago,IL,USA + Optional Inputs: + Mode Of Transport (inputs: driving, walking, bicycling, transit) + Default mode (no mode input) is driving + e.g. mode=driving or mode=transit + Units (metric or imperial) + e.g. units=metric + Restrictions (inputs: tolls, highways, ferries, indoor) + e.g. avoid=tolls + Departure Time (inputs: now or (YYYY, MM, DD, HH, MM, SS) departing) + e.g. departure_time=now or departure_time=2016,12,17,23,40,40 + Arrival Time (inputs: (YYYY, MM, DD, HH, MM, SS) arriving) + e.g. arrival_time=2016,12,17,23,40,40 + Languages: + Languages list: https://developers.google.com/maps/faq#languagesupport) + e.g. language=fr + + Sample request: + @commute origins=Chicago,IL,USA destinations=New+York,NY,USA language=fr + + Please note: + Fare information can be derived, though is solely dependent on the + availability of the informatipython run.py lib/followup.py --config-file ~/.zuliprc-localon released by public transport operators. + Duration in traffic can only be derived if a departure time is set. + If a location has spaces in its name, please use a + symbol in the + place of the space/s. + A departure time and a arrival time can not be inputted at the same time + To add more than 1 input for a category, + e.g. more than 1 destinations, + use (|), e.g. destinations=Empire+State+Building|Statue+Of+Liberty + No spaces within addresses. + Departure times and arrival times must be in the UTC timezone, + you can use the timezone bot. + ''' + + def triage_message(self, message): + # return True iff we want to (possibly) response to this message + original_content = message['content'] + # This next line of code is defensive, as we + # never want to get into an infinite loop of posting follow + # ups for own follow ups! + if message['display_recipient'] == 'commute': + return False + is_commute = original_content.startswith('@commute') + return is_commute + + # adds API Authentication Key to url request + def get_api_key(self): + # google-commute.ini must have been moved from + # ~/zulip/contrib_bots/lib/CommuteBot/google-commute.ini into + # /google-commute.ini for program to work + # see doc.md for more information + with open(CONFIG_PATH) as settings: + config = cp.ConfigParser() + config.readfp(settings) + return config.get('Google.com', 'api_key') + + # determines if bot will respond as a private message/ stream message + def send_info(self, message, letter, client): + if message['type'] == 'private': + client.send_message(dict( + type='private', + to=message['sender_email'], + content=letter, + )) + else: + client.send_message(dict( + type='stream', + subject=message['subject'], + to=message['display_recipient'], + content=letter, + )) + + def calculate_seconds(self, time_str): + times = time_str.split(',') + times = [int(x) for x in times] + # UNIX time from January 1, 1970 00:00:00 + unix_start_date = dt.datetime(1970, 1, 1, 0, 0, 0) + requested_time = dt.datetime(*times) + total_seconds = str(int((requested_time-unix_start_date) + .total_seconds())) + return total_seconds + + # adds departure time and arrival time paramaters into HTTP request + def add_time_to_params(self, params): + # limited to UTC timezone because of difficulty with user inputting + # correct string for input + if 'departure_time' in params: + if 'departure_time' != 'now': + params['departure_time'] = self.calculate_seconds(params['departure_time']) + elif 'arrival_time' in params: + params['arrival_time'] = self.calculate_seconds(params['arrival_time']) + return + + # gets content for output and sends it to user + def get_send_content(self, rjson, params, message, client): + try: + # JSON list of output variables + variable_list = rjson["rows"][0]["elements"][0] + # determines if user has valid inputs + not_found = (variable_list["status"] == "NOT_FOUND") + invalid_request = (rjson["status"] == "INVALID_REQUEST") + no_result = (variable_list["status"] == "ZERO_RESULTS") + + if no_result: + self.send_info(message, + "Zero results\nIf stuck, try '@commute help'.", + client) + return + elif not_found or invalid_request: + raise IndexError + except IndexError: + self.send_info(message, + "Invalid input, please see instructions." + "\nIf stuck, try '@commute help'.", client) + return + + # origin and destination strings + begin = 'From: ' + rjson["origin_addresses"][0] + end = 'To: ' + rjson["destination_addresses"][0] + distance = 'Distance: ' + variable_list["distance"]["text"] + duration = 'Duration: ' + variable_list["duration"]["text"] + output = begin + '\n' + end + '\n' + distance + # if user doesn't know that default mode is driving + if 'mode' not in params: + mode = 'Mode of Transport: Driving' + output += '\n' + mode + + # determines if fare information is available + try: + fare = ('Fare: ' + variable_list["fare"]["currency"] + + variable_list["fare"]["text"]) + output += '\n' + fare + except (KeyError, IndexError): + pass + + # determines if traffic duration information is available + try: + traffic_duration = ('Duration in traffic: ' + + variable_list["duration_in_traffic"] + ["text"]) + output += '\n' + traffic_duration + except (KeyError, IndexError): + output += '\n' + duration + + # bot sends commute information to user + self.send_info(message, output, client) + + # creates parameters for HTTP request + def parse_pair(self, content_list): + result = {} + for item in content_list: + # enables key value pair + org = item.split('=') + # ensures that invalid inputs are not entered into url request + if len(org) != 2: + continue + key, value = org + result[key] = value + return result + + def receive_response(self, params, message, client): + def validate_requests(request): + if request.status_code == 200: + return request.json() + else: + self.send_info(message, + "Something went wrong. Please try again." + + " Error: {error_num}.\n{error_text}" + .format(error_num=request.status_code, + error_text=request.text), client) + return + r = requests.get('https://maps.googleapis.com/maps/api/' + + 'distancematrix/json', params=params) + result = validate_requests(r) + return result + + def handle_message(self, message, client, state_handler): + original_content = message['content'] + content_list = original_content.split() + + if "help" in content_list: + self.send_info(message, self.help_info, client) + return + + params = self.parse_pair(content_list) + params['key'] = self.api_key + self.add_time_to_params(params) + + rjson = self.receive_response(params, message, client) + if not rjson: + return + + self.get_send_content(rjson, params, message, client) + +handler_class = CommuteHandler +handler = CommuteHandler() + +def test_parse_pair(): + result = handler.parse_pair(['departure_time=2016,12,20,23,59,00', + 'dog_foo=cat-foo']) + assert result == dict(departure_time='2016,12,20,23,59,00', + dog_foo='cat-foo') + +def test_calculate_seconds(): + result = handler.calculate_seconds('2016,12,20,23,59,00') + assert result == str(1482278340) + +def test_get_api_key(): + # must change to your own api key for test to work + result = handler.get_api_key() + assert result == 'abcdefghijksm' + +def test_helper_functions(): + test_parse_pair() + test_calculate_seconds() + test_get_api_key() + +if __name__ == '__main__': + test_helper_functions() + print('Success')