def testSMSLogSync(self): prev_couch_count = self.getSMSLogCount() prev_sql_count = self.getSMSCount() # Test Create smslog = SMSLog() self.setRandomSMSLogValues(smslog) smslog.save() sleep(1) self.assertEqual(self.getSMSLogCount(), prev_couch_count + 1) self.assertEqual(self.getSMSCount(), prev_sql_count + 1) sms = SMS.objects.get(couch_id=smslog._id) self.checkFieldValues(smslog, sms, SMSLog._migration_get_fields()) self.assertTrue(SMSLog.get_db().get_rev(smslog._id).startswith('1-')) # Test Update self.setRandomSMSLogValues(smslog) smslog.save() sleep(1) self.assertEqual(self.getSMSLogCount(), prev_couch_count + 1) self.assertEqual(self.getSMSCount(), prev_sql_count + 1) sms = SMS.objects.get(couch_id=smslog._id) self.checkFieldValues(smslog, sms, SMSLog._migration_get_fields()) self.assertTrue(SMSLog.get_db().get_rev(smslog._id).startswith('2-'))
def post(request, domain): # TODO: Figure out if this is being used anywhere and remove it if not """ We assume sms sent to HQ will come in the form http://hqurl.com?username=%(username)s&password=%(password)s&id=%(phone_number)s&text=%(message)s """ text = request.REQUEST.get('text', '') to = request.REQUEST.get('id', '') username = request.REQUEST.get('username', '') # ah, plaintext passwords.... # this seems to be the most common API that a lot of SMS gateways expose password = request.REQUEST.get('password', '') if not text or not to or not username or not password: error_msg = 'ERROR missing parameters. Received: %(1)s, %(2)s, %(3)s, %(4)s' % \ ( text, to, username, password ) logging.error(error_msg) return HttpResponseBadRequest(error_msg) user = authenticate(username=username, password=password) if user is None or not user.is_active: return HttpResponseBadRequest("Authentication fail") msg = SMSLog( domain=domain, # TODO: how to map phone numbers to recipients, when phone numbers are shared? #couch_recipient=id, phone_number=to, direction=INCOMING, date=datetime.now(), text=text) msg.save() return HttpResponse('OK')
def testSMSLogSync(self): self.deleteAllLogs() self.assertEqual(self.getSMSLogCount(), 0) self.assertEqual(self.getSMSCount(), 0) # Test Create smslog = SMSLog() self.setRandomSMSLogValues(smslog) smslog.save() sleep(1) self.assertEqual(self.getSMSLogCount(), 1) self.assertEqual(self.getSMSCount(), 1) sms = SMS.objects.get(couch_id=smslog._id) self.checkFieldValues(smslog, sms, SMSLog._migration_get_fields()) self.assertTrue(SMSLog.get_db().get_rev(smslog._id).startswith('1-')) # Test Update self.setRandomSMSLogValues(smslog) smslog.save() sleep(1) self.assertEqual(self.getSMSLogCount(), 1) self.assertEqual(self.getSMSCount(), 1) sms = SMS.objects.get(couch_id=smslog._id) self.checkFieldValues(smslog, sms, SMSLog._migration_get_fields()) self.assertTrue(SMSLog.get_db().get_rev(smslog._id).startswith('2-'))
def incoming(phone_number, text, backend_api, timestamp=None, domain_scope=None, backend_message_id=None, delay=True): """ entry point for incoming sms phone_number - originating phone number text - message content backend_api - backend API ID of receiving sms backend timestamp - message received timestamp; defaults to now (UTC) domain_scope - if present, only messages from phone numbers that can be definitively linked to this domain will be processed; others will be dropped (useful to provide security when simulating incoming sms) """ # Log message in message log phone_number = clean_phone_number(phone_number) msg = SMSLog( phone_number = phone_number, direction = INCOMING, date = timestamp or datetime.utcnow(), text = text, domain_scope = domain_scope, backend_api = backend_api, backend_message_id = backend_message_id, ) if settings.SMS_QUEUE_ENABLED: msg.processed = False msg.datetime_to_process = datetime.utcnow() msg.queued_timestamp = msg.datetime_to_process msg.save() enqueue_directly(msg) else: msg.processed = True msg.save() process_incoming(msg, delay=delay) return msg
def send_sms_to_verified_number(verified_number, text): """ Sends an sms using the given verified phone number entry. verified_number The VerifiedNumber entry to use when sending. text The text of the message to send. return True on success, False on failure """ try: backend = verified_number.backend module = __import__(backend.outbound_module, fromlist=["send"]) kwargs = backend.outbound_params msg = SMSLog( couch_recipient_doc_type = verified_number.owner_doc_type, couch_recipient = verified_number.owner_id, phone_number = "+" + str(verified_number.phone_number), direction = OUTGOING, date = datetime.utcnow(), domain = verified_number.domain, text = text ) try: msg.backend_api = module.API_ID except Exception: pass module.send(msg, **kwargs) msg.save() return True except Exception as e: logging.exception("Exception while sending SMS to VerifiedNumber id " + verified_number._id) return False
def send_sms(domain, id, phone_number, text): """ Sends an outbound SMS. Returns false if it fails. """ if phone_number is None: return False if isinstance(phone_number, int) or isinstance(phone_number, long): phone_number = str(phone_number) logging.debug('Sending message: %s' % text) phone_number = clean_phone_number(phone_number) msg = SMSLog(domain=domain, couch_recipient=id, couch_recipient_doc_type="CouchUser", phone_number=phone_number, direction=OUTGOING, date = datetime.utcnow(), text = text) try: api = get_backend_api(msg) try: msg.backend_api = api.API_ID except Exception: pass api.send(msg) msg.save() return True except Exception: logging.exception("Problem sending SMS to %s" % phone_number) return False
def post(request, domain): # TODO: Figure out if this is being used anywhere and remove it if not """ We assume sms sent to HQ will come in the form http://hqurl.com?username=%(username)s&password=%(password)s&id=%(phone_number)s&text=%(message)s """ text = request.REQUEST.get('text', '') to = request.REQUEST.get('id', '') username = request.REQUEST.get('username', '') # ah, plaintext passwords.... # this seems to be the most common API that a lot of SMS gateways expose password = request.REQUEST.get('password', '') if not text or not to or not username or not password: error_msg = 'ERROR missing parameters. Received: %(1)s, %(2)s, %(3)s, %(4)s' % \ ( text, to, username, password ) logging.error(error_msg) return HttpResponseBadRequest(error_msg) user = authenticate(username=username, password=password) if user is None or not user.is_active: return HttpResponseBadRequest("Authentication fail") msg = SMSLog(domain=domain, # TODO: how to map phone numbers to recipients, when phone numbers are shared? #couch_recipient=id, phone_number=to, direction=INCOMING, date = datetime.now(), text = text) msg.save() return HttpResponse('OK')
def incoming(phone_number, text, backend_api, timestamp=None, domain_scope=None, delay=True): """ entry point for incoming sms phone_number - originating phone number text - message content backend_api - backend ID of receiving sms backend timestamp - message received timestamp; defaults to now (UTC) domain_scope - if present, only messages from phone numbers that can be definitively linked to this domain will be processed; others will be dropped (useful to provide security when simulating incoming sms) """ phone_number = clean_phone_number(phone_number) v = VerifiedNumber.by_phone(phone_number, include_pending=True) if domain_scope: # only process messages for phones known to be associated with this domain if v is None or v.domain != domain_scope: raise RuntimeError("attempted to simulate incoming sms from phone number not verified with this domain") # Log message in message log msg = SMSLog( phone_number=phone_number, direction=INCOMING, date=timestamp or datetime.utcnow(), text=text, backend_api=backend_api, ) if v is not None and v.verified: msg.couch_recipient_doc_type = v.owner_doc_type msg.couch_recipient = v.owner_id msg.domain = v.domain msg.save() create_billable_for_sms(msg, backend_api, delay=delay) if v is not None and v.verified: for h in settings.SMS_HANDLERS: try: handler = to_function(h) except: logging.exception("error loading sms handler: %s" % h) continue try: was_handled = handler(v, text) except: logging.exception("unhandled error in sms handler %s for message [%s]" % (h, text)) was_handled = False if was_handled: break else: if not process_sms_registration(msg): import verify verify.process_verification(phone_number, text) return msg
def _get_fake_sms(self, text): msg = SMSLog(domain=self.domain, phone_number='+16175555454', direction=OUTGOING, date=datetime.utcnow(), backend_id=self.mobile_backend.get_id, text=text) msg.save() return msg
def create_outgoing_sms(self, backend): sms = SMSLog( domain=self.domain, date=datetime.utcnow(), direction='O', phone_number='9991234567', text='message', backend_id=backend.couch_id ) sms.save() return sms
def _get_fake_sms(self, text): msg = SMSLog( domain=self.domain, phone_number='+16175555454', direction=OUTGOING, date=datetime.utcnow(), backend_id=self.mobile_backend.get_id, text=text ) msg.save() return msg
def send_test_message(verified_number, text, metadata=None): msg = SMSLog(couch_recipient_doc_type=verified_number.owner_doc_type, couch_recipient=verified_number.owner_id, phone_number="+" + str(verified_number.phone_number), direction=OUTGOING, date=datetime.utcnow(), domain=verified_number.domain, text=text, processed=True, datetime_to_process=datetime.utcnow(), queued_timestamp=datetime.utcnow()) msg.save() add_msg_tags(msg, metadata) return True
def incoming(phone_number, text, backend_api, timestamp=None, domain_scope=None, backend_message_id=None, delay=True, backend_attributes=None, raw_text=None): """ entry point for incoming sms phone_number - originating phone number text - message content backend_api - backend API ID of receiving sms backend timestamp - message received timestamp; defaults to now (UTC) domain_scope - if present, only messages from phone numbers that can be definitively linked to this domain will be processed; others will be dropped (useful to provide security when simulating incoming sms) """ # Log message in message log if text is None: text = "" phone_number = clean_phone_number(phone_number) msg = SMSLog( phone_number=phone_number, direction=INCOMING, date=timestamp or datetime.utcnow(), text=text, domain_scope=domain_scope, backend_api=backend_api, backend_message_id=backend_message_id, raw_text=raw_text, ) if backend_attributes: for k, v in backend_attributes.items(): setattr(msg, k, v) if settings.SMS_QUEUE_ENABLED: msg.processed = False msg.datetime_to_process = datetime.utcnow() msg.queued_timestamp = msg.datetime_to_process msg.save() enqueue_directly(msg) else: msg.processed = True msg.save() process_incoming(msg, delay=delay) return msg
def send_test_message(verified_number, text, metadata=None): msg = SMSLog( couch_recipient_doc_type=verified_number.owner_doc_type, couch_recipient=verified_number.owner_id, phone_number="+" + str(verified_number.phone_number), direction=OUTGOING, date=datetime.utcnow(), domain=verified_number.domain, text=text, processed=True, datetime_to_process=datetime.utcnow(), queued_timestamp=datetime.utcnow(), ) msg.save() add_msg_tags(msg, metadata) return True
def create_from_request(request, delay=True): """ From an inbound request (representing an incoming message), create a message (log) object with the right fields populated. """ sender = request.REQUEST.get(InboundParams.SENDER, "") message = request.REQUEST.get(InboundParams.MESSAGE, "") timestamp = request.REQUEST.get(InboundParams.TIMESTAMP, "") # parse date or default to current utc time actual_timestamp = datetime.strptime(timestamp, DATE_FORMAT) \ if timestamp else datetime.utcnow() # not sure yet if this check is valid is_unicode = request.REQUEST.get(InboundParams.UDHI, "") == "1" if is_unicode: message = message.decode("hex").decode("utf_16_be") # if you don't have an exact match for either of these fields, save nothing domains = domains_for_phone(sender) domain = domains[0] if len(domains) == 1 else "" recipients = users_for_phone(sender) recipient = recipients[0].get_id if len(recipients) == 1 else "" log = SMSLog(couch_recipient=recipient, couch_recipient_doc_type="CouchUser", phone_number=sender, direction=INCOMING, date=actual_timestamp, text=message, domain=domain, backend_api=API_ID) log.save() try: # attempt to bill client from hqbilling.tasks import bill_client_for_sms from hqbilling.models import UnicelSMSBillable if delay: bill_client_for_sms.delay(UnicelSMSBillable, log._id) else: bill_client_for_sms(UnicelSMSBillable, log._id) except Exception as e: logging.debug("UNICEL API contacted, errors in billing. Error: %s" % e) return log
def arbitrary_messages_by_backend_and_direction(backend_ids, phone_number=None, domain=None, directions=DIRECTIONS): phone_number = phone_number or TEST_NUMBER domain = domain or TEST_DOMAIN messages = [] for api_id, instance_id in backend_ids.items(): for direction in directions: sms_log = SMSLog(direction=direction, phone_number=phone_number, domain=domain, backend_api=api_id, backend_id=instance_id, text=arbitrary_message(), date=datetime.datetime.utcnow()) sms_log.save() messages.append(sms_log) return messages
def arbitrary_messages_by_backend_and_direction(backend_ids, phone_number=None, domain=None, directions=DIRECTIONS): phone_number = phone_number or TEST_NUMBER domain = domain or TEST_DOMAIN messages = [] for api_id, instance_id in backend_ids.items(): for direction in directions: sms_log = SMSLog( direction=direction, phone_number=phone_number, domain=domain, backend_api=api_id, backend_id=instance_id, text=arbitrary_message(), date=datetime.datetime.now() ) sms_log.save() messages.append(sms_log) return messages
def testFRISMSLogSync(self): prev_couch_count = self.getSMSLogCount() prev_sql_count = self.getSMSCount() # Test Create smslog = SMSLog() self.setRandomSMSLogValues(smslog) smslog.save() sleep(1) smslog = FRISMSLog.get(smslog._id) self.setRandomFRISMSLogValues(smslog) smslog.save() sleep(1) self.assertEqual(self.getSMSLogCount(), prev_couch_count + 1) self.assertEqual(self.getSMSCount(), prev_sql_count + 1) sms = SMS.objects.get(couch_id=smslog._id) self.checkFieldValues(smslog, sms, FRISMSLog._migration_get_fields()) self.assertTrue(FRISMSLog.get_db().get_rev(smslog._id).startswith('2-')) # Test Update self.setRandomSMSLogValues(smslog) self.setRandomFRISMSLogValues(smslog) smslog.save() sleep(1) self.assertEqual(self.getSMSLogCount(), prev_couch_count + 1) self.assertEqual(self.getSMSCount(), prev_sql_count + 1) sms = SMS.objects.get(couch_id=smslog._id) self.checkFieldValues(smslog, sms, FRISMSLog._migration_get_fields()) self.assertTrue(SMSLog.get_db().get_rev(smslog._id).startswith('3-'))
def send_sms_with_backend(domain, phone_number, text, backend_id): msg = SMSLog(domain = domain, phone_number = phone_number, direction = OUTGOING, date = datetime.utcnow(), text = text) if backend_id == "MOBILE_BACKEND_MACH": try: try: msg.backend_api = mach_api.API_ID except Exception: pass mach_api.send(msg) msg.save() return True except Exception: logging.exception("Exception while sending SMS to %s with backend %s" % (phone_number, backend_id)) return False elif backend_id == "MOBILE_BACKEND_UNICEL": try: try: msg.backend_api = unicel_api.API_ID except Exception: pass unicel_api.send(msg) msg.save() return True except Exception: logging.exception("Exception while sending SMS to %s with backend %s" % (phone_number, backend_id)) return False else: try: backend = MobileBackend.get(backend_id) except Exception: backend = None if backend is None: return False try: module = __import__(backend.outbound_module, fromlist=["send"]) try: msg.backend_api = module.API_ID except Exception: pass kwargs = backend.outbound_params module.send(msg, **kwargs) msg.save() return True except Exception as e: logging.exception("Exception while sending SMS to %s with backend %s" % (phone_number, backend_id)) return False
def testOutgoingInternationalTropoApi(self): self.assertEqual(self.tropo_rate_any.conversion_rate, self.usd_rate.conversion) msg = SMSLog(domain = self.domain, direction = OUTGOING, date = datetime.datetime.utcnow(), text = self.test_message) msg.save() if self.sms_config and self.sms_config.get("tropo_int") and self.tropo_backend: logging.info("\n\n[Billing - LIVE] Tropo: Outgoing International SMS Test") msg.phone_number = self.sms_config.get("tropo_int") msg.save() data = self.tropo_backend.send(msg, delay=False) else: logging.info("\n\n[Billing] Tropo: Outgoing International SMS Test") data = "<session><success>true</success><token>faketoken</token><id>aadfg3Aa321gdc8e628df2\n</id></session>" msg.phone_number = "+4915253271951" msg.save() bill_client_for_sms(TropoSMSBillable, msg.get_id, **dict(response=data)) logging.info("[Billing] Response from TROPO: %s" % data) tropo_id = TropoSMSBillable.get_tropo_id(data) logging.info("[Billing] TROPO ID: %s" % tropo_id) billable_items = TropoSMSBillable.by_domain(self.domain) if billable_items: billable_item = billable_items[0] self.assertEqual(self.tropo_rate_any.base_fee, billable_item.billable_amount) self.assertEqual(self.tropo_rate_any._id, billable_item.rate_id) self.assertEqual(msg._id, billable_item.log_id) self.assertEqual(self.tropo_rate_any.conversion_rate, billable_item.conversion_rate) self.assertEqual(self.dimagi_surcharge.base_fee, billable_item.dimagi_surcharge) self.assertEqual(tropo_id, billable_item.tropo_id) billable_item.delete() if billable_item.has_error: raise Exception("There were recorded errors creating an outgoing International Tropo billing rate: %s" % billable_item.error_message) else: raise Exception("There were unknown errors creating an outgoing International Tropo billing rate!")
def testOutgoingUnicelApi(self): self.assertEqual(self.unicel_rate.conversion_rate, self.usd_rate.conversion) msg = SMSLog(domain = self.domain, direction = OUTGOING, date = datetime.datetime.utcnow(), text = self.test_message) msg.save() if self.sms_config and self.sms_config.get("unicel") and self.unicel_backend: logging.info("\n\n[Billing - LIVE] UNICEL: Outgoing SMS Test.") msg.phone_number = self.sms_config.get("unicel") msg.save() data = self.unicel_backend.send(msg, delay=False) else: logging.info("\n\n[Billing] UNICEL: Outgoing SMS Test") data = "successful23541253235" msg.phone_number = "+555555555" msg.save() bill_client_for_sms(UnicelSMSBillable, msg.get_id, **dict(response=data)) logging.info("Response from UNICEL: %s" % data) billable_items = UnicelSMSBillable.by_domain_and_direction(self.domain, OUTGOING) if billable_items: billable_item = billable_items[0] self.assertEqual(self.unicel_rate.base_fee * self.unicel_rate.conversion_rate, billable_item.billable_amount) self.assertEqual(self.unicel_rate._id, billable_item.rate_id) self.assertEqual(msg._id, billable_item.log_id) self.assertEqual(self.unicel_rate.conversion_rate, billable_item.conversion_rate) self.assertEqual(self.dimagi_surcharge.base_fee, billable_item.dimagi_surcharge) self.assertEqual(data, billable_item.unicel_id) if billable_item.has_error: raise Exception("There were recorded errors creating an outgoing UNICEL billing rate: %s" % billable_item.error_message) else: raise Exception("There were unknown errors creating an outgoing UNICEL billing rate!")
def incoming(phone_number, text, backend_api): phone_without_plus = str(phone_number) if phone_without_plus[0] == "+": phone_without_plus = phone_without_plus[1:] phone_with_plus = "+" + phone_without_plus # Circular Import from corehq.apps.reminders.models import SurveyKeyword v = VerifiedNumber.view("sms/verified_number_by_number", key=phone_without_plus, include_docs=True ).one() # Log message in message log msg = SMSLog( phone_number = phone_with_plus, direction = INCOMING, date = datetime.utcnow(), text = text, backend_api = backend_api ) if v is not None: msg.couch_recipient_doc_type = v.owner_doc_type msg.couch_recipient = v.owner_id msg.domain = v.domain msg.save() # Handle incoming sms if v is not None: session = XFormsSession.view("smsforms/open_sessions_by_connection", key=[v.domain, v.owner_id], include_docs=True).one() text_words = text.upper().split() # Respond to "#START <keyword>" command if len(text_words) > 0 and text_words[0] == "#START": if len(text_words) > 1: sk = SurveyKeyword.get_keyword(v.domain, text_words[1]) if sk is not None: if session is not None: session.end(False) session.save() start_session_from_keyword(sk, v) else: send_sms_to_verified_number(v, "Survey '" + text_words[1] + "' not found.") else: send_sms_to_verified_number(v, "Usage: #START <keyword>") # Respond to "#STOP" keyword elif len(text_words) > 0 and text_words[0] == "#STOP": if session is not None: session.end(False) session.save() # Respond to "#CURRENT" keyword elif len(text_words) > 0 and text_words[0] == "#CURRENT": if session is not None: resp = current_question(session.session_id) send_sms_to_verified_number(v, resp.event.text_prompt) # Respond to unknown command elif len(text_words) > 0 and text_words[0][0] == "#": send_sms_to_verified_number(v, "Unknown command '" + text_words[0] + "'") # If there's an open session, treat the inbound text as the answer to the next question elif session is not None: resp = current_question(session.session_id) event = resp.event valid = False error_msg = None # Validate select questions if event.datatype == "select": try: answer = int(text.strip()) if answer >= 1 and answer <= len(event._dict["choices"]): valid = True except Exception: pass if not valid: error_msg = "Invalid Response. " + event.text_prompt # For now, anything else passes else: valid = True if valid: responses = get_responses(msg) if len(responses) > 0: response_text = format_message_list(responses) send_sms_to_verified_number(v, response_text) else: send_sms_to_verified_number(v, error_msg) # Try to match the text against a keyword to start a survey elif len(text_words) > 0: sk = SurveyKeyword.get_keyword(v.domain, text_words[0]) if sk is not None: start_session_from_keyword(sk, v) else: #TODO: Registration via SMS pass
def incoming(phone_number, text, backend_api, timestamp=None, domain_scope=None, delay=True): """ entry point for incoming sms phone_number - originating phone number text - message content backend_api - backend ID of receiving sms backend timestamp - message received timestamp; defaults to now (UTC) domain_scope - if present, only messages from phone numbers that can be definitively linked to this domain will be processed; others will be dropped (useful to provide security when simulating incoming sms) """ phone_number = clean_phone_number(phone_number) v = VerifiedNumber.by_phone(phone_number, include_pending=True) if domain_scope: # only process messages for phones known to be associated with this domain if v is None or v.domain != domain_scope: raise RuntimeError( 'attempted to simulate incoming sms from phone number not verified with this domain' ) # Log message in message log msg = SMSLog(phone_number=phone_number, direction=INCOMING, date=timestamp or datetime.utcnow(), text=text, backend_api=backend_api) if v is not None and v.verified: msg.couch_recipient_doc_type = v.owner_doc_type msg.couch_recipient = v.owner_id msg.domain = v.domain msg.save() create_billable_for_sms(msg, backend_api, delay=delay) if v is not None and v.verified: for h in settings.SMS_HANDLERS: try: handler = to_function(h) except: logging.exception('error loading sms handler: %s' % h) continue try: was_handled = handler(v, text) except: logging.exception( 'unhandled error in sms handler %s for message [%s]' % (h, text)) was_handled = False if was_handled: break else: if not process_sms_registration(msg): import verify verify.process_verification(phone_number, text) return msg