Source code for main

from functools import wraps
from flask import Flask, request, jsonify, Response, abort
import json, requests, urllib.parse,urllib.request
import filecmp, difflib, shutil
import numpy as np
from google.cloud import datastore
import datetime
from remove_ignore_entities import removeIgnoreEntities
from datahandle import requestLocationAndMeal, requestItem

app = Flask(__name__)

###Helper functions

#Authentication
def check_auth(name, passw):
    return (name=='user' and passw=='pass')
def requires_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            abort(401)
        return f(*args, **kwargs)
    return decorated

[docs]def isPartialTerm(search, filename): """Checks if input term is part of a larger official term by looking for any matches with split up versions of regular terms. :param search: The searched term (e.g. 'north quad') :type search: string :param filename: Name of the file the term is being searched for in ('LocationExtra.txt') :type filename: string """ list_data = open(filename) for i in list_data: if search.upper() == str(i.rstrip("\n\r")).upper(): return True return False
[docs]def similarSearch(search, category): """Handles user input that doesn't match official terms exactly using `isPartialTerm`. If input ``search`` is a partial term of any official terms, returns list of recommended official terms. :param search: The searched term (e.g. 'north quad') :type search: string :param category: Entity category of the search term ('Location'/'Meal') :type filename: string """ #Check type of term and adjust file searched accordingly if category.upper() == 'LOCATION': extrasFilename = 'LocationExtra.txt' mainFilename = 'LocationMain.txt' elif category.upper() == 'MEAL': extrasFilename = 'MealExtra.txt' mainFilename = 'MealMain.txt' else: return "File error" #Check if input term is part of a larger official term if isPartialTerm(search, extrasFilename) == False: return "Found" #If it is, suggest possible full terms #Go through list of full terms and check for possibilities list_data = open(mainFilename, 'r') possible_searches = [] for i in list_data: if search.upper() in str(i.rstrip("\n\r")).upper(): possible_searches.append(str(i.rstrip("\n\r"))) #Suggest list of possibilities outputstring = "Did you mean " for i in possible_searches: outputstring += i outputstring += ' or ' return outputstring[:-4] + '?'
[docs]def findLocationAndMeal(req_data): """Dialogflow ``findLocationAndMeal`` intent handler. Checks for valid Location and Meal and sends HTTP response with appropriate data. :param req_data: Dialogflow POST request data :type req_data: JSON """ parameters = {} locationEntered = False mealEntered = False meal_in = "" loc_in = "" #Check for Location and Meal parameters in Dialogflow request if req_data['queryResult']['parameters']['Location']: category = 'Location' search = req_data['queryResult']['parameters']['Location'] loc_in = search parameters[category] = category locationEntered = True if req_data['queryResult']['parameters']['Meal']: if locationEntered == False: category = 'Meal' search = req_data['queryResult']['parameters']['Meal'] meal_in = search parameters[category] = category mealEntered = True #Error if neither are found if (locationEntered == False) and (mealEntered == False): search = 'Error' category = 'Error' #Handle search for location or meal, location first if both parameters are in request text = similarSearch(search, category) #Setting up response data responsedata = { 'fulfillmentText': text } #Text is valid if text == 'Found': validParams = True #If both location and meal parameters included in request, now handle meal if locationEntered and mealEntered: category = 'Meal' search = req_data['queryResult']['parameters']['Meal'] meal_in = search text = similarSearch(search, category) if text == 'Found': parameters[category] = category else: validParams = False #If location/meal/both search is fully valid, send response triggering Dialogflow event if validParams: outputcontextparams = req_data['queryResult']['outputContexts'][0]['parameters'] if len(parameters) == 1: if 'Location' in parameters and (outputcontextparams['Meal'] == ''): eventname = 'valid_location' responsedata = { 'fulfillmentText': 'What meal would you like?' } elif 'Meal' in parameters and (outputcontextparams['Location'] == ''): eventname = 'valid_meal' responsedata = { 'fulfillmentText': 'Which dining location?' } else: if 'Location' not in parameters: loc_in = req_data['queryResult']['outputContexts'][0]['parameters']['Location'] if 'Meal' not in parameters: meal_in = req_data['queryResult']['outputContexts'][0]['parameters']['Meal'] date_in = datetime.date.today() responsedata['fulfillmentText'] = requestLocationAndMeal(date_in, loc_in, meal_in) else: date_in = datetime.date.today() responsedata['fulfillmentText'] = requestLocationAndMeal(date_in, loc_in, meal_in) #Else, send suggestion to user for the parameter that was invalid #If both parameters invalid, prioritize location else: responsedata['fulfillmentText'] = text return responsedata
#findItem intent handling def findItem(req_data): date_in = datetime.date.today() loc_in = req_data['queryResult']['parameters']['Location'] item_in = req_data['queryResult']['parameters']['any'] if 'Meal' in req_data['queryResult']['parameters']: meal_in = req_data['queryResult']['parameters']['Meal'] else: meal_in = '' return requestItem(date_in, loc_in, meal_in, item_in) ######################################################################### ###Primary Handler Functions #Basic homepage for checking successful deployment
[docs]@app.route('/') def home(): """Web app home page for quick successful deployment check. """ return "Success"
#Webhook call
[docs]@app.route('/webhook',methods=['POST']) @requires_auth def webhookPost(): """Dialogflow webhook POST Request handler requiring authentication. Uses `findLocationAndMeal` or `findItem` intent handlers and returns appropriate JSON response. """ req_data = request.get_json() intentname = req_data['queryResult']['intent']['displayName'] if intentname == 'findLocationAndMeal': responsedata = findLocationAndMeal(req_data) elif intentname == 'findItem': responsedata = findItem(req_data) return jsonify ( responsedata )
#Google Cron update handler
[docs]@app.route('/cron',methods=['POST']) def cronUpdate(): """Google Cloud Platform scheduled CRON request handler. Checks for changes to MDining API data (Location/Meal), sends notification to Slack channel if change detected. Ignores changes to specified terms in ``ignore.json`` file. Authenticates requests by checking for user and passw in POST request body. """ #Cron authentication through post request data req_data = request.get_json() responsedata = '' #Get secret values from Datastore environment variables client = datastore.Client() query = client.query(kind = 'env_vars') entity = query.fetch() secrets = list(entity)[0] slackurl = secrets.get('slack_api') passw = secrets.get('pass') user = secrets.get('user') if (req_data['user'] != user) or (req_data['pass'] != passw): message = 'Authentication failed.' else: with open('ignore.json') as f: ignoredata = json.load(f) mealchanged = False locationchanged = False #Meal Diff mealreq = requests.post('http://api.studentlife.umich.edu/menu/menu_generator/meal.php') mealdata = mealreq.json() newmeal = [] for entry in mealdata: if entry['optionValue'] != "": newmeal.append(entry['optionValue']) originalmealfile = open('MealMain.txt', 'r').readlines() originalmeal = [] for entry in originalmealfile: originalmeal.append(entry.strip('\n')) originalmeal = np.array(removeIgnoreEntities(originalmeal, 'Meal')) newmeal = np.array(removeIgnoreEntities(newmeal, 'Meal')) mealremoved = np.setdiff1d(originalmeal, newmeal).tolist() mealadded = np.setdiff1d(newmeal, originalmeal).tolist() mealchanged = False if bool(mealremoved) or bool(mealadded): mealchanged = True #Location Diff locationreq = requests.post('http://api.studentlife.umich.edu/menu/menu_generator/location.php') locationdata = locationreq.json() newlocation = [] for entry in locationdata: if entry['optionValue'] != "": newlocation.append(entry['optionValue']) originallocationfile = open('LocationMain.txt', 'r').readlines() originallocation = [] for entry in originallocationfile: originallocation.append(entry.strip('\n')) originallocation = np.array(removeIgnoreEntities(originallocation, 'Location')) newlocation = np.array(removeIgnoreEntities(newlocation, 'Location')) locationremoved = np.setdiff1d(originallocation, newlocation).tolist() locationadded = np.setdiff1d(newlocation, originallocation).tolist() locationchanged = False if bool(locationremoved) or bool(locationadded): locationchanged = True #Check for file changes for appropriate response to slack if needed if locationchanged or mealchanged: slackresponse = {} slackresponse['attachments'] = [] message = "Update needed for " if locationchanged: message += "location" if bool(locationadded): newlocationsstr = '' for i in locationadded: newlocationsstr = newlocationsstr + i + '\n' slackresponse['attachments'].append( { "title": "New locations", "text": newlocationsstr } ) if bool(locationremoved): removedlocationsstr = '' for i in locationremoved: removedlocationsstr = removedlocationsstr + i + '\n' slackresponse['attachments'].append( { "title": "Removed locations", "text": removedlocationsstr } ) if mealchanged: if bool(mealadded): newmealsstr = '' for i in mealadded: newmealsstr = newmealsstr + i + '\n' slackresponse['attachments'].append( { "title": "New meals", "text": newmealsstr } ) if bool(mealremoved): removedmealsstr = '' for i in mealremoved: removedmealsstr = removedmealsstr + i + '\n' slackresponse['attachments'].append( { "title": "Removed meals", "text": removedmealsstr } ) if locationchanged: message += " and meal" else: message += "meal" message += " in m-voice." slackresponse['text'] = message slackpostdata = requests.post(slackurl, json=slackresponse) else: message = "Data up to date" return jsonify( message=message, locationadded=locationadded, locationremoved=locationremoved, mealadded=mealadded, mealremoved=mealremoved )