diff --git a/config.py.sample b/config.py.sample index dbe08fb..50e5c76 100644 --- a/config.py.sample +++ b/config.py.sample @@ -9,13 +9,13 @@ DB_USER = '' DB_PASS = '' DB_NAME = 'tekstmelding' -# Authenticates Inside -INSIDE_API_KEY = '' +API_KEYS = { + 'somesecretkey': 'appname' +} MEMBERSHIP_PRICE_KR = 1 -## Logging ## -############# +# Logging LOGGING = { 'version': 1, @@ -52,8 +52,7 @@ LOGGING = { } } -## Sendega ## -############# +# Sendega # Endpoint for HTTP POST/GET SENDEGA_URL = 'https://smsc.sendega.com/SendSms.ashx' diff --git a/dusken.py b/dusken.py new file mode 100644 index 0000000..faee27d --- /dev/null +++ b/dusken.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import requests + + +class Dusken(object): + """ Dusken API """ + + url = None + api_key = None + + def __init__(self, url, api_key): + self.url = url + self.api_key = api_key + + @property + def dusken_auth(self): + return {'Authorization': 'Token {}'.format(self.api_key)} + + def get_user_by_phone(self, phone_number): + url = '{}users/'.format(self.url) + payload = {'phone_number': phone_number} + users = requests.get(url, params=payload, headers=self.dusken_auth).json() + if users.get('count') == 1: + return users.get('results')[0] + return None + + def get_full_name(self, user): + full_name = '{} {}'.format( + user.get('first_name', '').strip(), + user.get('last_name', '').strip() + ) + return full_name[:50].strip() diff --git a/requirements.txt b/requirements.txt index a7a298b..f224cf0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Flask>=0.12 mysqlclient==1.3.10 suds-jurko==0.6 +requests diff --git a/tekstmelding.py b/tekstmelding.py index 294aaea..dd5ffce 100644 --- a/tekstmelding.py +++ b/tekstmelding.py @@ -7,8 +7,9 @@ import datetime import logging import logging.config -from utils import generate_activation_code, MyJSONEncoder +from utils import require_token, MyJSONEncoder import sendega +import dusken import config @@ -145,19 +146,6 @@ def update_event(event_id, outgoing_id): [outgoing_id, event_id]) -def log_dlr(**kwargs): - return query_db(""" - INSERT INTO dlr - (msgid, extID, msisdn, status, statustext, - registered, sent, delivered, errorcode, - errormessage, operatorerrorcode) - VALUES ( - %(msgid)s, %(extID)s, %(msisdn)s, %(status)s, %(statustext)s, - %(registered)s, %(sent)s, %(delivered)s, %(errorcode)s, - %(errormessage)s, %(operatorerrorcode)s) - """, kwargs, lastrowid=True) - - def send_sms(destination, content, incoming_id=None, billing=False, price=0, activation_code=None): args = get_sendega().create_sms( @@ -187,11 +175,11 @@ def send_sms(destination, content, incoming_id=None, def notify_valid_membership(incoming_id=None, number=None, user=None): assert user - name = get_inside().get_full_name(user) - if user.get('expires_lifelong'): + name = dusken.get_full_name(user) + if user.get('is_lifelong_member'): expires = 'verdens undergang' else: - expires = str(user.get('expires')) + expires = str(user.get('last_membership').get('end_date')) content = u"Hei %(name)s! Ditt medlemskap er gyldig til %(expires)s. Last ned app: %(app)s" % ({ 'name': name, @@ -216,18 +204,14 @@ def notify_valid_membership(incoming_id=None, number=None, user=None): @app.route('/kassa/new-membership-card', methods=['POST']) +@require_token def kassa_notify_new_card(): """ Notify user Adds a pending membership to specified number """ - api_key = request.args.get('api_key') - data = request.get_json() phone_number = data.get('phone_number') card_number = data.get('card_number') action = data.get('action') # FIXME: not used - if api_key != app.config['INSIDE_API_KEY']: - abort(403) # Forbidden - if action is None or card_number is None: return jsonify(**{'error': 'Missing required param action or card_number'}), 400 @@ -258,6 +242,32 @@ def kassa_notify_new_card(): return jsonify(**{'result': 'SMS sent OK', 'content': content, 'outgoing_id': outgoing_id}) +@app.route('/send', methods=['POST']) +@require_token +def send(): + data = request.get_json() + number = data.get('to') + message = data.get('message') + + if not number or not message: + return jsonify(**{'error': 'Missing required param number or message'}), 400 + + if number[0] == '+': + number = number.replace('+', '') + + if not number.isdigit(): + return jsonify(**{'error': "Param number is not numerical '{}'".format(number)}), 400 + + app.logger.info('Sending to number={}, service={}, message={}'.format( + number, g.user, message)) + + outgoing_id = send_sms(destination=number, content=message) + + log_event(action='send', outgoing_id=outgoing_id) + + return jsonify(**{'result': 'sent', 'message': message, 'outgoing_id': outgoing_id}) + + @app.route('/sendega-incoming', methods=['POST']) def incoming(): args = {'ip': request.headers.get('X-Real-IP') or request.remote_addr} @@ -280,10 +290,10 @@ def incoming(): # What the fuck, man, that's not a phone number, man. abort(400) # Bad Request - if not keyword in ('DNS', 'DNSMEDLEM'): + if keyword not in ('DNS', 'DNSMEDLEM'): abort(400) # Bad Request - if not shortcode in ('2454'): + if shortcode not in ('2454'): abort(400) # Bad Request incoming_id = log_incoming(**args) @@ -292,15 +302,13 @@ def incoming(): "Incoming SMS from number:%s saved with id:%s", args['msisdn'], incoming_id) - user = get_inside().get_user_by_phone(msisdn) + user = dusken.get_user_by_phone(msisdn) if user: - expired = get_inside().user_is_expired(user) - - if not expired: + if user.get('is_member'): return notify_valid_membership( incoming_id=incoming_id, number=msisdn, user=user) - elif expired: + else: return renew_membership( incoming_id=incoming_id, number=msisdn, user=user) else: @@ -316,81 +324,6 @@ def incoming(): app.logger.error('Unhandled SMS, incoming_id=%s', incoming_id) -@app.route('/sendega-dlr', methods=['POST']) -def dlr(): - args = {'ip': request.headers.get('X-Real-IP') or request.remote_addr} - - for key in ('msgid', 'extID', 'msisdn', 'status', 'statustext', - 'registered', 'sent', 'delivered', - 'errorcode', 'errormessage', 'operatorerrorcode'): - args[key] = request.form.get(key) - - app.logger.debug("Dlr, args: %s", args) - - dlr_id = log_dlr(**args) - - # We need to check what the original message was all about - incoming_id = args['extID'] - - success = (args['status'] == '4') - if not success: - return notify_could_not_charge( - incoming_id=incoming_id, dlr_id=dlr_id, number=args['msisdn']) - - event = query_db(""" - SELECT * FROM event - WHERE incoming_id=%s - AND action IN ('new_membership', 'renew_membership') - """, [incoming_id], one=True) - - if not event: - app.logger.error('Got an unknown delivery report, dlr_id=%s', dlr_id) - return 'what' - - action = event['action'] - - if action == 'renew_membership': - return renew_membership_delivered( - incoming_id=incoming_id, dlr_id=dlr_id, user_id=event['user_id']) - elif action == 'new_membership': - return new_membership_delivered( - incoming_id=incoming_id, dlr_id=dlr_id) - - app.logger.error('Unhandled DLR, dlr_id=%s', dlr_id) - - -@app.route('/stats/memberships/', methods=['GET']) -def stats_memberships(): - start_datetime = request.args.get('start', '2015-08-01') - sale_events = query_db(""" - SELECT DATE_FORMAT(timestamp, '%%Y-%%m-%%d') as date,count(*) as sales FROM event - WHERE action IN ('new_membership_delivered', 'renew_membership_delivered') - AND timestamp > %s - GROUP BY DATE_FORMAT(timestamp, '%%Y-%%m-%%d') - ORDER BY timestamp""", [start_datetime]) - - result = {'meta': {'num_results': len(sale_events)}, 'memberships': sale_events} - headers = {'Access-Control-Allow-Origin': '*'} - return (jsonify(**result), 200, headers) - - -@app.route('/stats/memberships/series', methods=['GET']) -def stats_memberships_stats(): - start_datetime = request.args.get('start', '2015-08-01') - sale_events = query_db(""" - SELECT DATE_FORMAT(timestamp, '%%Y-%%m-%%d %%T') as date FROM event - WHERE action IN ('new_membership_delivered', 'renew_membership_delivered') - AND timestamp > %s - ORDER BY timestamp""", [start_datetime]) - - result = { - 'meta': {'num_results': len(sale_events)}, - 'memberships': [x['date'] for x in sale_events] - } - headers = {'Access-Control-Allow-Origin': '*'} - return (jsonify(**result), 200, headers) - - @app.route('/') def main(): return 'Tekstmelding!' diff --git a/utils.py b/utils.py index 8795441..826997a 100644 --- a/utils.py +++ b/utils.py @@ -1,14 +1,7 @@ -import string -import random from datetime import datetime from flask.json import JSONEncoder - - -def generate_activation_code(length=5): - chars = string.ascii_lowercase + string.digits - # remove easily misinterpreted chars - chars = chars.translate(None, '0oil1wv') - return ''.join(random.choice(chars) for _ in range(length)) +from flask import current_app, request, abort, g +from functools import wraps class MyJSONEncoder(JSONEncoder): @@ -17,3 +10,19 @@ class MyJSONEncoder(JSONEncoder): return o.isoformat() return super(MyJSONEncoder, self).default(o) + + +def require_token(func): + @wraps(func) + def check_token(*args, **kwargs): + if 'Authorization' not in request.headers: + abort(401) + auth_type, token = request.headers.get('Authorization').split(' ') + if auth_type != 'Token': + abort(401) + users = current_app.config.get('API_KEYS', {}) + if token not in users.keys(): + abort(401) + g.user = users.get(token) + return func(*args, **kwargs) + return check_token