diff --git a/access_control_decorator.py b/access_control_decorator.py new file mode 100755 index 00000000..28d81afe --- /dev/null +++ b/access_control_decorator.py @@ -0,0 +1,45 @@ +from datetime import timedelta +from flask import make_response, request, current_app +from functools import update_wrapper + + +def crossdomain(origin=None, methods=None, headers=None, + max_age=21600, attach_to_all=True, + automatic_options=True): + if methods is not None: + methods = ', '.join(sorted(x.upper() for x in methods)) + if headers is not None and not isinstance(headers, basestring): + headers = ', '.join(x.upper() for x in headers) + if not isinstance(origin, basestring): + origin = ', '.join(origin) + if isinstance(max_age, timedelta): + max_age = max_age.total_seconds() + + def get_methods(): + if methods is not None: + return methods + + options_resp = current_app.make_default_options_response() + return options_resp.headers['allow'] + + def decorator(f): + def wrapped_function(*args, **kwargs): + if automatic_options and request.method == 'OPTIONS': + resp = current_app.make_default_options_response() + else: + resp = make_response(f(*args, **kwargs)) + if not attach_to_all and request.method != 'OPTIONS': + return resp + + h = resp.headers + + h['Access-Control-Allow-Origin'] = origin + h['Access-Control-Allow-Methods'] = get_methods() + h['Access-Control-Max-Age'] = str(max_age) + if headers is not None: + h['Access-Control-Allow-Headers'] = headers + return resp + + f.provide_automatic_options = False + return update_wrapper(wrapped_function, f) + return decorator \ No newline at end of file diff --git a/app.py b/app.py index a3beba8b..939f81a8 100755 --- a/app.py +++ b/app.py @@ -20,6 +20,8 @@ from models import db, aggregate_stats, log_call, call_count from political_data import PoliticalData from cache_handler import CacheHandler +from fftf_leaderboard import FFTFLeaderboard +from access_control_decorator import crossdomain app = Flask(__name__) @@ -34,6 +36,8 @@ # Optional Redis cache, for caching Google spreadsheet campaign overrides cache_handler = CacheHandler(app.config['REDIS_URL']) +# FFTF Leaderboard handler. Only used if FFTF Leadboard params are passed in +leaderboard = FFTFLeaderboard(app.debug, app.config['FFTF_LB_ASYNC_POOL_SIZE']) call_methods = ['GET', 'POST'] @@ -68,7 +72,13 @@ def parse_params(r): 'userPhone': r.values.get('userPhone'), 'campaignId': r.values.get('campaignId', 'default'), 'zipcode': r.values.get('zipcode', None), - 'repIds': r.values.getlist('repIds') + 'repIds': r.values.getlist('repIds'), + + # optional values for Fight for the Future Leaderboards + # if present, these add extra logging functionality in call_complete + 'fftfCampaign': r.values.get('fftfCampaign'), + 'fftfReferer': r.values.get('fftfReferer'), + 'fftfSession': r.values.get('fftfSession') } # lookup campaign by ID @@ -120,7 +130,7 @@ def make_calls(params, campaign): """ Connect a user to a sequence of congress members. Required params: campaignId, repIds - Optional params: zipcode, + Optional params: zipcode, fftfCampaign, fftfReferer, fftfSession """ resp = twilio.twiml.Response() @@ -145,6 +155,7 @@ def _make_calls(): @app.route('/create', methods=call_methods) +@crossdomain(origin='*') def call_user(): """ Makes a phone call to a user. @@ -154,6 +165,9 @@ def call_user(): Optional Params: zipcode repIds + fftfCampaign + fftfReferer + fftfSession """ # parse the info needed to make the call params, campaign = parse_params(request) @@ -181,6 +195,7 @@ def call_user(): @app.route('/connection', methods=call_methods) +@crossdomain(origin='*') def connection(): """ Call handler to connect a user with their congress person(s). @@ -189,6 +204,9 @@ def connection(): Optional Params: zipcode repIds (if not present go to incoming_call flow and asked for zipcode) + fftfCampaign + fftfReferer + fftfSession """ params, campaign = parse_params(request) @@ -216,6 +234,7 @@ def incoming_call(): """ Handles incoming calls to the twilio numbers. Required Params: campaignId + Optional Params: fftfCampaign, fftfReferer, fftfSession Each Twilio phone number needs to be configured to point to: server.com/incoming_call?campaignId=12345 @@ -314,6 +333,10 @@ def call_complete(): log_call(params, campaign, request) + # If FFTF Leaderboard params are present, log this call + if params['fftfCampaign'] and params['fftfReferer']: + leaderboard.log_call(params, campaign, request) + resp = twilio.twiml.Response() i = int(request.values.get('call_index', 0)) @@ -321,6 +344,10 @@ def call_complete(): if i == len(params['repIds']) - 1: # thank you for calling message play_or_say(resp, campaign['msg_final_thanks']) + + # If FFTF Leaderboard params are present, log the call completion status + if params['fftfCampaign'] and params['fftfReferer']: + leaderboard.log_complete(params, campaign, request) else: # call the next representative params['call_index'] = i + 1 # increment the call counter @@ -344,7 +371,10 @@ def call_complete_status(): 'phoneNumber': request.values.get('To', ''), 'callStatus': request.values.get('CallStatus', 'unknown'), 'repIds': params['repIds'], - 'campaignId': params['campaignId'] + 'campaignId': params['campaignId'], + 'fftfCampaign': params['fftfCampaign'], + 'fftfReferer': params['fftfReferer'], + 'fftfSession': params['fftfSession'] }) diff --git a/config.py b/config.py index da2ca8eb..742d97cb 100755 --- a/config.py +++ b/config.py @@ -27,6 +27,9 @@ class Config(object): # limit on the amount of time to ring before giving up TW_TIMEOUT = 40 # seconds + # number of threads to limit asynchronous leaderboard requests + FFTF_LB_ASYNC_POOL_SIZE = 8 + SECRET_KEY = 'AOUSBDAONPSOMDASIDUBSDOUABER)*#(R&(&@@#))' diff --git a/data/campaigns.yaml b/data/campaigns.yaml index 0ec67801..735a1bf7 100755 --- a/data/campaigns.yaml +++ b/data/campaigns.yaml @@ -159,7 +159,7 @@ msg_between_thanks: Thanks msg_final_thanks: https://shutthebackdoor.net/call-tool/msg_final_thanks.mp3 -- id: battle-for-the-net +- id: battleforthenet numbers: - 650-614-5872 target_house: true diff --git a/fftf_leaderboard.py b/fftf_leaderboard.py new file mode 100755 index 00000000..08a13ee9 --- /dev/null +++ b/fftf_leaderboard.py @@ -0,0 +1,73 @@ +import json +import grequests + +class FFTFLeaderboard(): + + debug_mode = False + pool_size = 1 + + def __init__(self, debug_mode, pool_size): + + self.debug_mode = debug_mode + + def log_call(self, params, campaign, request): + + if params['fftfCampaign'] == None or params['fftfReferer'] == None: + return + + i = int(request.values.get('call_index')) + + kwds = { + 'campaign_id': campaign['id'], + 'member_id': params['repIds'][i], + 'zipcode': params['zipcode'], + 'phone_number': params['userPhone'], + 'call_id': request.values.get('CallSid', None), + 'status': request.values.get('DialCallStatus', 'unknown'), + 'duration': request.values.get('DialCallDuration', 0) + } + data = json.dumps(kwds) + + self.post_to_leaderboard( + params['fftfCampaign'], + 'call', + data, + params['fftfReferer'], + params['fftfSession']) + + def log_complete(self, params, campaign, request): + + if params['fftfCampaign'] == None or params['fftfReferer'] == None: + return + + self.post_to_leaderboard( + params['fftfCampaign'], + 'calls_complete', + 'yay', + params['fftfReferer'], + params['fftfSession']) + + def post_to_leaderboard(self, fftf_campaign, stat, data, host, session): + + debug_mode = self.debug_mode + + def finished(res, **kwargs): + if debug_mode: + print "FFTF Leaderboard call complete: %s" % res + + data = { + 'campaign': fftf_campaign, + 'stat': stat, + 'data': data, + 'host': host, + 'session': session + } + + if self.debug_mode: + print "FFTF Leaderboard sending: %s" % data + + url = 'https://leaderboard.fightforthefuture.org/log' + req = grequests.post(url, data=data, hooks=dict(response=finished)) + job = grequests.send(req, grequests.Pool(self.pool_size)) + + return diff --git a/requirements.txt b/requirements.txt index b075cf8c..2268c2af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ argparse==1.2.1 blinker==1.3 gevent==1.0 greenlet==0.4.2 +grequests==0.2.0 httplib2==0.8 itsdangerous==0.23 mysql-connector-python==1.2.2