#!/usr/bin/env python # -*- coding: utf-8 -*- from flask import Flask, request, g, abort import MySQLdb import MySQLdb.cursors import datetime import logging from utils import generate_activation_code import sendega import inside import config app = Flask(__name__) app.config.from_object('config') if not app.debug: # Log INFO and higher levels to stderr stream_handler = logging.StreamHandler() formatter = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s") stream_handler.setFormatter(formatter) stream_handler.setLevel(logging.INFO) app.logger.addHandler(stream_handler) # Email admins about ERRORs and higher levels from logging.handlers import SMTPHandler mail_handler = SMTPHandler( mailhost='127.0.0.1', fromaddr='noreply+tekstmelding@neuf.no', toaddrs=app.config['ADMINS'], subject='Tekstmelding FAIL') mail_handler.setLevel(logging.ERROR) app.logger.addHandler(mail_handler) def connect_db(): """Connects to our database.""" db = MySQLdb.connect( host=app.config['DB_HOST'], user=app.config['DB_USER'], passwd=app.config['DB_PASS'], db=app.config['DB_NAME'], cursorclass=MySQLdb.cursors.DictCursor, charset='utf8') return db def get_db(): """Returns a database connection. Creates one if there is none for the current application context.""" if not hasattr(g, 'db'): g.db = connect_db() return g.db @app.teardown_appcontext def close_db(error): """Closes the database connection at the end of the request.""" if hasattr(g, 'db'): g.db.close() def query_db(query, args=(), one=False, lastrowid=False): """Queries the database. Will use an existing or new database connection, create a cursor, execute the query, fetch all rows, close the cursor then return the rows. """ assert not (one and lastrowid) # app.logger.debug('Got query with args: %s', query, args) cur = get_db().cursor() cur.execute(query, args) # app.logger.debug('Executed query: %s', cur._last_executed) if lastrowid: ret = cur.lastrowid else: rv = cur.fetchall() ret = (rv[0] if rv else None) if one else rv get_db().commit() cur.close() return ret def get_sendega(): """Returns a Sendega object. Creates one if there is none for the current application context.""" if not hasattr(g, 'sendega'): g.sendega = sendega.Sendega( wsdl=config.SENDEGA_WSDL, username=config.SENDEGA_USERNAME, password=config.SENDEGA_PASSWORD, sender_bulk=config.SENDEGA_SENDER_BULK, sender_billing=config.SENDEGA_SENDER_BILLING, dlr_url=config.SENDEGA_DLR) return g.sendega def get_inside(): if not hasattr(g, 'inside'): g.inside = inside.Inside( host=app.config['INSIDE_DB_HOST'], username=app.config['INSIDE_DB_USER'], password=app.config['INSIDE_DB_PASS'], db=app.config['INSIDE_DB_NAME']) return g.inside def log_incoming(**kwargs): return query_db(""" INSERT INTO incoming (msgid, msisdn, msg, mms, mmsdata, shortcode, mcc, mnc, pricegroup, keyword, keywordid, errorcode, errormessage, registered, ip) VALUES (%(msgid)s, %(msisdn)s, %(msg)s, %(mms)s, %(mmsdata)s, %(shortcode)s, %(mcc)s, %(mnc)s, %(pricegroup)s, %(keyword)s, %(keywordid)s, %(errorcode)s, %(errormessage)s, %(registered)s, %(ip)s) """, kwargs, lastrowid=True) def log_outgoing(**kwargs): return query_db(""" INSERT INTO outgoing (sender, destination, pricegroup, content, contentTypeID, contentHeader, dlrUrl, ageLimit, extID, sendDate, refID, priority, gwID, pid, dcs) VALUES (%(sender)s, %(destination)s, %(pricegroup)s, %(content)s, %(contentTypeID)s, %(contentHeader)s, %(dlrUrl)s, %(ageLimit)s, %(extID)s, %(sendDate)s, %(refID)s, %(priority)s, %(gwID)s, %(pid)s, %(dcs)s) """, kwargs, lastrowid=True) def log_outgoing_response(**kwargs): return query_db(""" INSERT INTO outgoing_response (id, MessageID, Success, ErrorNumber, ErrorMessage) VALUES (%(id)s, %(MessageID)s, %(Success)s, %(ErrorNumber)s, %(ErrorMessage)s) """, kwargs, lastrowid=True) def log_event(**kwargs): for key in ('incoming_id', 'outgoing_id', 'dlr_id', 'action', 'activation_code', 'user_id'): kwargs.setdefault(key, None) return query_db(""" INSERT INTO event (incoming_id, outgoing_id, dlr_id, action, user_id, activation_code) VALUES (%(incoming_id)s, %(outgoing_id)s, %(dlr_id)s, %(action)s, %(user_id)s, %(activation_code)s) """, kwargs, lastrowid=True) 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( destination=destination, content=content, reference=incoming_id, billing=billing, price=price) app.logger.debug('Outgoing, args: %s', args) outgoing_id = log_outgoing(**args) if app.debug: return outgoing_id response = get_sendega().send(**args) response['id'] = outgoing_id log_outgoing_response(**response) if response.get('Success', None): return outgoing_id return None def notify_valid_membership(incoming_id=None, number=None, user=None): assert user name = get_inside().get_full_name(user) expires = 'verdens undergang' if user.get('expires_lifelong') else str(user.get('expires')) content = u"Hei %(name)s! Ditt medlemskap er gyldig til %(expires)s. Last ned app: %(app)s" % ({ 'name': name, 'expires': expires, 'app': 'http://snappo.com/app', }) outgoing_id = send_sms( destination=number, content=content, incoming_id=incoming_id) app.logger.info("Sent SMS to number:%s with proof of membership", number) log_event( action='notify_valid_membership', incoming_id=incoming_id, outgoing_id=outgoing_id, user_id=user['id']) return 'OK' def notify_payment_options_new(incoming_id=None, number=None): content = u"Du kan kjøpe medlemskap via SnappOrder: http://snappo.com/app (200,-) eller ved å sende DNSMEDLEM til 2454 (230,-)" outgoing_id = send_sms( destination=number, content=content, incoming_id=incoming_id) app.logger.info("Sent SMS to number:%s with payment options (new)", number) log_event( action='notify_payment_options_new', incoming_id=incoming_id, outgoing_id=outgoing_id) return 'OK' def notify_payment_options_renewal(incoming_id=None, number=None, user=None): content = u"Ditt medlemskap er utløpt. Det kan fornyes via SnappOrder: http://snappo.com/app (200,-) eller ved å sende DNSMEDLEM til 2454 (230,-)" outgoing_id = send_sms( destination=number, content=content, incoming_id=incoming_id) app.logger.info( "Sent SMS to number:%s with payment options (renewal)", number) log_event( action='notify_payment_options_renewal', incoming_id=incoming_id, outgoing_id=outgoing_id, user_id=user['id']) return 'OK' def notify_could_not_charge(incoming_id=None, dlr_id=None, number=None): app.logger.warning( "Attempt to charge number:%s as a response to incoming_id:%s failed", number, incoming_id) content = u"Beklager, bestillingen kunne ikke gjennomføres. Spørsmål? medlem@studentersamfundet.no" outgoing_id = send_sms( destination=number, content=content, incoming_id=incoming_id) log_event( action='notify_could_not_charge', incoming_id=incoming_id, outgoing_id=outgoing_id, dlr_id=dlr_id) return 'OK' def renew_membership(incoming_id=None, number=None, user=None): new_expire = datetime.date.today() + datetime.timedelta(days=365) content = u"Hei %(name)s! Ditt medlemskap er nå gyldig ut %(new_expire)s. Spørsmål? medlem@studentersamfundet.no" % ({ 'name': get_inside().get_full_name(user), 'new_expire': str(new_expire), }) outgoing_id = send_sms( destination=number, content=content, incoming_id=incoming_id, billing=True, price=app.config['MEMBERSHIP_PRICE_KR'] * 100, ) app.logger.info( "Membership renewal requested by userid:%s number:%s new_expire:%s", user['id'], number, new_expire) log_event( action='renew_membership', incoming_id=incoming_id, outgoing_id=outgoing_id, user_id=user['id']) return 'OK' def renew_membership_delivered(incoming_id=None, dlr_id=None, user_id=None): new_expire = datetime.date.today() + datetime.timedelta(days=365) get_inside().renew_user(user_id=user_id, new_expire=new_expire) app.logger.info( "Response to incoming_id:%s was delivered, user_id:%s renewed to %s", incoming_id, user_id, new_expire) log_event( action='renew_membership_delivered', incoming_id=incoming_id, dlr_id=dlr_id) return 'OK' def new_membership(incoming_id=None, number=None): activation_code = generate_activation_code() content = u"Velkommen! Dette er et midlertidig medlemsbevis. Aktiver medlemskapet ditt her: https://s.neuf.no/sms/%(number)s/%(activation_code)s" % ({ 'number': number, 'activation_code': activation_code, }) outgoing_id = send_sms( destination=number, content=content, incoming_id=incoming_id, billing=True, price=app.config['MEMBERSHIP_PRICE_KR'] * 100) app.logger.info( "New membership requested by number:%s, activation code sent", number) log_event( action='new_membership', incoming_id=incoming_id, outgoing_id=outgoing_id, activation_code=activation_code) return 'OK' def new_membership_delivered(incoming_id=None, dlr_id=None): app.logger.info( "Response to incoming_id:%s was delivered, new membership", incoming_id) log_event( action='new_membership_delivered', incoming_id=incoming_id, dlr_id=dlr_id) return 'OK' @app.route('/') def main(): return 'Tekstmelding!' @app.route('/sendega-incoming') def incoming(): args = {'ip': request.remote_addr} for key in ('msgid', 'msisdn', 'msg', 'mms', 'mmsdata', 'shortcode', 'mcc', 'mnc', 'pricegroup', 'keyword', 'keywordid', 'errorcode', 'errormessage', 'registered'): args[key] = request.args.get(key, None) app.logger.debug("Incoming, args: %s", args) if None in (args['msisdn'], args['keyword'], args['shortcode']): abort(400) # Bad Request msisdn = args['msisdn'] keyword = args['keyword'].strip().upper() shortcode = args['shortcode'] if not msisdn.isdigit(): # What the fuck, man, that's not a phone number, man. abort(400) # Bad Request if not keyword in ('DNS', 'DNSMEDLEM'): abort(400) # Bad Request if not shortcode in ('2454'): abort(400) # Bad Request incoming_id = log_incoming(**args) app.logger.info( "Incoming SMS from number:%s saved with id:%s", args['msisdn'], incoming_id) user = get_inside().get_user_by_phone(msisdn) if user: expired = get_inside().user_is_expired(user) if keyword == 'DNS': if not expired: return notify_valid_membership( incoming_id=incoming_id, number=msisdn, user=user) elif expired: return notify_payment_options_renewal( incoming_id=incoming_id, number=msisdn, user=user) elif keyword == 'DNSMEDLEM': if not expired: return notify_valid_membership( incoming_id=incoming_id, number=msisdn, user=user) elif expired: return renew_membership( incoming_id=incoming_id, number=msisdn, user=user) else: if keyword == 'DNS': return notify_payment_options_new( incoming_id=incoming_id, number=msisdn) elif keyword == 'DNSMEDLEM': return new_membership( incoming_id=incoming_id, number=msisdn) app.logger.error('Unhandled SMS, incoming_id=%s', incoming_id) @app.route('/sendega-dlr') def dlr(): args = {'ip': request.remote_addr} for key in ('msgid', 'extID', 'msisdn', 'status', 'statustext', 'registered', 'sent', 'delivered', 'errorcode', 'errormessage', 'operatorerrorcode'): args[key] = request.args.get(key, None) dlr_id = log_dlr(**args) # We need to check what the original message was all about incoming_id = args['extID'] event = query_db('SELECT * FROM event WHERE incoming_id=%s', incoming_id, one=True) if not event: app.logger.error('Got an unknown delivery report, dlr_id=%s', dlr_id) return 'what' success = (args['status'] == 4) action = event['action'] if not success: return notify_could_not_charge( incoming_id=incoming_id, dlr_id=dlr_id, number=args['msisdn']) 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) if __name__ == '__main__': app.run() # For WSGI application = app