def handle_outbound_message(self, message): if random.random() > self.ack_rate: yield self.publish_nack(message['message_id'], 'Not accepted by network') return dr = ('failed' if random.random() < self.failure_rate else 'delivered') log.info('MT %(dr)s: %(from_addr)s -> %(to_addr)s: %(content)s' % { 'dr': dr, 'from_addr': message['from_addr'], 'to_addr': message['to_addr'], 'content': message['content'], }) yield self.publish_ack(message['message_id'], sent_message_id=uuid.uuid4().hex) yield self.publish_delivery_report(message['message_id'], dr) if random.random() < self.reply_rate: reply_copy = self.reply_copy or message['content'] log.info('MO %(from_addr)s -> %(to_addr)s: %(content)s' % { 'from_addr': message['to_addr'], 'to_addr': message['from_addr'], 'content': reply_copy, }) yield self.publish_message( message_id=uuid.uuid4().hex, content=reply_copy, to_addr=message['from_addr'], from_addr=message['to_addr'], provider='devnull', session_event=TransportUserMessage.SESSION_NONE, transport_type=self.transport_type, transport_metadata={})
def process_command_send_message(self, user_account_key, conversation_key, command_data, **kwargs): if kwargs: log.info("Received unexpected command args: %s" % (kwargs,)) conv = yield self.get_conversation(user_account_key, conversation_key) if conv is None: log.warning("Cannot find conversation '%s' for user '%s'." % ( conversation_key, user_account_key)) return log.info('Processing send_message: %s' % kwargs) to_addr = command_data['to_addr'] content = command_data['content'] msg_options = command_data['msg_options'] in_reply_to = msg_options.pop('in_reply_to', None) self.add_conv_to_msg_options(conv, msg_options) if in_reply_to: msg = yield self.vumi_api.mdb.get_inbound_message(in_reply_to) if msg: yield self.reply_to( msg, content, helper_metadata=msg_options['helper_metadata']) else: log.warning('Unable to reply, message %s does not exist.' % ( in_reply_to)) else: yield self.send_to( to_addr, content, endpoint='default', **msg_options)
def send_event_to_client(self, event, conversation, push_url): if push_url is None: log.info( "push_event_url not configured for conversation: %s" % ( conversation.key)) return return self.push(push_url, event)
def process_command_send_message(self, user_account_key, conversation_key, command_data, **kwargs): if kwargs: log.info("Received unexpected command args: %s" % (kwargs, )) conv = yield self.get_conversation(user_account_key, conversation_key) if conv is None: log.warning("Cannot find conversation '%s' for user '%s'." % (conversation_key, user_account_key)) return log.info('Processing send_message: %s' % kwargs) to_addr = command_data['to_addr'] content = command_data['content'] msg_options = command_data['msg_options'] in_reply_to = msg_options.pop('in_reply_to', None) self.add_conv_to_msg_options(conv, msg_options) if in_reply_to: msg = yield self.vumi_api.mdb.get_inbound_message(in_reply_to) if msg: yield self.reply_to( msg, content, helper_metadata=msg_options['helper_metadata']) else: log.warning('Unable to reply, message %s does not exist.' % (in_reply_to)) else: yield self.send_to(to_addr, content, endpoint='default', **msg_options)
def send_message(self, batch_id, to_addr, content, msg_options): msg = yield self.send_to(to_addr, content, endpoint='default', **msg_options) yield self.vumi_api.mdb.add_outbound_message(msg, batch_id=batch_id) log.info('Stored outbound %s' % (msg, ))
def send_scheduled_messages(self, conv): config = self.get_config_for_conversation(conv) messages = config.messages message_options = {} conv.set_go_helper_metadata( message_options.setdefault('helper_metadata', {})) for contacts in (yield conv.get_opted_in_contact_bunches( conv.delivery_class)): for contact in (yield contacts): index_key = 'scheduled_message_index_%s' % (conv.key, ) message_index = int(contact.extra[index_key] or '0') if message_index >= len(messages): # We have nothing more to send to this person. continue to_addr = contact.addr_for(conv.delivery_class) if not to_addr: log.info("No suitable address found for contact %s %r" % ( contact.key, contact, )) continue yield self.send_message(conv.batch.key, to_addr, messages[message_index], message_options) contact.extra[index_key] = u'%s' % (message_index + 1) yield contact.save()
def send_scheduled_messages(self, conv): config = self.get_config_for_conversation(conv) messages = config.messages message_options = {} conv.set_go_helper_metadata( message_options.setdefault('helper_metadata', {})) for contacts in (yield conv.get_opted_in_contact_bunches( conv.delivery_class)): for contact in (yield contacts): index_key = 'scheduled_message_index_%s' % (conv.key,) message_index = int(contact.extra[index_key] or '0') if message_index >= len(messages): # We have nothing more to send to this person. continue to_addr = contact.addr_for(conv.delivery_class) if not to_addr: log.info("No suitable address found for contact %s %r" % ( contact.key, contact,)) continue yield self.send_message( conv.batch.key, to_addr, messages[message_index], message_options) contact.extra[index_key] = u'%s' % (message_index + 1) yield contact.save()
def handle_send_to_endpoint(self, api, command): """ Sends a message to a specified endpoint. Command fields: - ``content``: The body of the reply message. - ``to_addr``: The address of the recipient (e.g. an MSISDN). - ``endpoint``: The name of the endpoint to send the message via. Reply fields: - ``success``: ``true`` if the operation was successful, otherwise ``false``. Example: .. code-block:: javascript api.request( 'outbound.send_to_endpoint', {content: 'Welcome!', to_addr: '+27831234567', endpoint: 'sms'}, function(reply) { api.log_info('Message sent: ' + reply.success); }); """ if not 'content' in command: return self._mkfaild( command, reason=u"'content' must be given in sends.") if not isinstance(command['content'], (unicode, type(None))): return self._mkfaild( command, reason=u"'content' must be unicode or null.") if not isinstance(command.get('endpoint'), unicode): return self._mkfaild( command, reason=u"'endpoint' must be given in sends.") if not isinstance(command.get('to_addr'), unicode): return self._mkfaild( command, reason=u"'to_addr' must be given in sends.") endpoint = command['endpoint'] content = command['content'] to_addr = command['to_addr'] conv = self.app_worker.conversation_for_api(api) if endpoint not in conv.extra_endpoints: return self._mkfaild( command, reason="Endpoint %r not configured" % (endpoint,)) msg_options = {} self.app_worker.add_conv_to_msg_options(conv, msg_options) log.info("Sending outbound message to %r via endpoint %r, content: %r" % (to_addr, endpoint, content)) d = self.app_worker.send_to( to_addr, content, endpoint=endpoint, **msg_options) d.addCallback(lambda r: self.reply(command, success=True)) d.addErrback(lambda f: self._mkfail(command, unicode(f.getErrorMessage()))) return d
def consume_control_command(self, cmd): worker_name = cmd.get('worker_name') publisher = self.worker_publishers.get(worker_name) if publisher: yield publisher.publish_message(cmd) log.info('Sent %s to %s' % (cmd, worker_name)) else: log.error('No worker publisher available for %s' % (cmd, ))
def consume_control_command(self, cmd): worker_name = cmd.get('worker_name') publisher = self.worker_publishers.get(worker_name) if publisher: yield publisher.publish_message(cmd) log.info('Sent %s to %s' % (cmd, worker_name)) else: log.error('No worker publisher available for %s' % (cmd,))
def handle_send_to_tag(self, api, command): """ Sends a message to a specified tag. Command fields: - ``content``: The body of the reply message. - ``to_addr``: The address of the recipient (e.g. an MSISDN). - ``tagpool``: The name of the tagpool to send the message via. - ``tag``: The name of the tag (within the tagpool) to send the message from. Your Go user account must have the tag acquired. Reply fields: - ``success``: ``true`` if the operation was successful, otherwise ``false``. Example: .. code-block:: javascript api.request( 'outbound.send_to_tag', {content: 'Welcome!', to_addr: '+27831234567', tagpool: 'vumi_long', tag: 'default10001'}, function(reply) { api.log_info('Message sent: ' + reply.success); }); """ tag = (command.get('tagpool'), command.get('tag')) content = command.get('content') to_addr = command.get('to_addr') if any(not isinstance(u, unicode) for u in (tag[0], tag[1], content, to_addr)): returnValue( self._mkfail(command, reason="Tag, content or to_addr not specified")) log.info("Sending outbound message to %r via tag %r, content: %r" % (to_addr, tag, content)) conv = self.app_worker.conversation_for_api(api) tags = [ tuple(endpoint.split(":", 1)) for endpoint in conv.extra_endpoints ] if tag not in tags: returnValue( self._mkfail(command, reason="Tag %r not held by account" % (tag, ))) msg_options = {} self.app_worker.add_conv_to_msg_options(conv, msg_options) endpoint = ':'.join(tag) yield self.app_worker.send_to(to_addr, content, endpoint=endpoint, **msg_options) returnValue(self.reply(command, success=True))
def setup_transport(self): config = self.get_static_config() log.info('Starting ParlayX transport: %s' % (self.transport_name,)) self.web_resource = yield self.start_web_resources( [(SmsNotificationService(self.handle_raw_inbound_message, self.publish_delivery_report), config.web_notification_path)], config.web_notification_port) self._parlayx_client = self._create_client(config) if config.start_notifications: yield self._parlayx_client.start_sms_notification()
def add_entry(self, src_conn, src_endpoint, dst_conn, dst_endpoint): src_conn = _to_conn(src_conn) dst_conn = _to_conn(dst_conn) self.validate_entry(src_conn, src_endpoint, dst_conn, dst_endpoint) connector_dict = self._routing_table.setdefault(str(src_conn), {}) if src_endpoint in connector_dict: log.info( "Replacing routing entry for (%r, %r): was %r, now %r" % ( str(src_conn), src_endpoint, connector_dict[src_endpoint], [str(dst_conn), dst_endpoint])) connector_dict[src_endpoint] = [str(dst_conn), dst_endpoint]
def process_command_reconcile_cache(self, conversation_key, user_account_key): key_tuple = (conversation_key, user_account_key) if key_tuple in self._cache_recon_conversations: log.info("Ignoring conversation %s for user %s because the " "previous cache recon run is still going." % (conversation_key, user_account_key)) return self._cache_recon_conversations.add(key_tuple) user_api = self.get_user_api(user_account_key) yield self.reconcile_cache(user_api, conversation_key) self._cache_recon_conversations.remove(key_tuple)
def process_command_reconcile_cache(self, conversation_key, user_account_key): key_tuple = (conversation_key, user_account_key) if key_tuple in self._cache_recon_conversations: log.info("Ignoring conversation %s for user %s because the " "previous cache recon run is still going." % ( conversation_key, user_account_key)) return self._cache_recon_conversations.add(key_tuple) user_api = self.get_user_api(user_account_key) yield self.reconcile_cache(user_api, conversation_key) self._cache_recon_conversations.remove(key_tuple)
def handle_raw_inbound_message(self, message_id, request): request_data = self.get_request_data_dict(request) values, errors = self.get_field_values( request_data, self.EXPECTED_FIELDS, self.OPTIONAL_FIELDS, # pass this in for error checking ) optional_values = self.get_optional_field_values( request_data, self.OPTIONAL_FIELDS ) if errors: log.info('Unhappy incoming message: %s ' % (errors,)) yield self.finish_request( message_id, json.dumps(errors), code=http.BAD_REQUEST ) return if values['type'] == self.REQUEST_TYPE['response']: # resume session session_event = TransportUserMessage.SESSION_RESUME elif values['type'] == self.REQUEST_TYPE['request']: # new session session_event = TransportUserMessage.SESSION_NEW else: # TODO #4: handle self.REQUEST_TYPE['release'] and # self.REQUEST_TYPE['timeout']. # Default to new session for now. session_event = TransportUserMessage.SESSION_NEW if optional_values['msg'] is not None: content = optional_values['msg'] else: content = None log.info( 'BlastSMSUssdTransport receiving inbound message from %s to ' '%s.' % (values['msisdn'], values['shortcode'])) yield self.publish_message( message_id=message_id, content=content, to_addr=values['shortcode'], from_addr=values['msisdn'], session_event=session_event, transport_type=self.TRANSPORT_TYPE, transport_name=self.TRANSPORT_NAME, transport_metadata={ 'sessionid': values['sessionid'], 'appid': optional_values['appid'], }, )
def handle_event(self, event, handler_config): """ From an account and conversation, this finds a batch and a tag. While there could be multiple batches per conversation and multiple tags per batch, this assumes lists of one, and takes the first entry of each list TODO: Turn this into a generic API Command Sender? An Example of a event_dispatcher.yaml config file, with mapped conversations in the config: transport_name: event_dispatcher event_handlers: sign_up_handler: go.vumitools.handler.SendMessageCommandHandler account_handler_configs: '73ad76ec8c2e40858dc9d6b934049d95': - - ['a6a20571e77f4aa89a8b10a771b005bc', sign_up] - - [ sign_up_handler, { worker_name: 'multi_survey_application', conversation_key: 'a6a20571e77f4aa89a8b10a771b005bc' } ] """ log.info( "SendMessageCommandHandler handling event: %s with config: %s" % (event, handler_config)) user_api = self.get_user_api(event.payload['account_key']) conv = yield user_api.conversation_store.get_conversation_by_key( event.payload['conversation_key']) conv = user_api.wrap_conversation(conv) command_data = event.payload['content'] command_data['batch_id'] = conv.batch.key command_data['msg_options'] = { 'helper_metadata': { 'go': { 'user_account': event.payload['account_key'] }, }, } sm_cmd = VumiApiCommand.command( handler_config['worker_name'], "send_message", command_data=command_data, conversation_key=handler_config['conversation_key'], account_key=event.payload['account_key']) log.info("Publishing command: %s" % sm_cmd) self.dispatcher.api_command_publisher.publish_message(sm_cmd)
def create_wechat_menu(self, access_token, menu_structure): url = self.make_url("menu/create", {"access_token": access_token}) response = yield http_request_full( url, method="POST", data=json.dumps(menu_structure), headers={"Content-Type": ["application/json"]} ) if not http_ok(response): raise WeChatApiException("Received HTTP code: %r when creating the menu." % (response.code,)) data = json.loads(response.delivered_body) if data["errcode"] != 0: raise WeChatApiException( "Received errcode: %(errcode)s, errmsg: %(errmsg)s " "when creating WeChat Menu." % data ) log.info("WeChat Menu created succesfully.")
def handle_raw_inbound_message(self, message_id, request): values, errors = self.get_field_values( request, self.EXPECTED_FIELDS, self.OPTIONAL_FIELDS, ) optional_values = self.get_optional_field_values( request, self.OPTIONAL_FIELDS ) if errors: log.info('Unhappy incoming message: %s ' % (errors,)) yield self.finish_request( message_id, json.dumps(errors), code=http.BAD_REQUEST ) return from_addr = values['msisdn'] provider = self.normalise_provider(values['provider']) ussd_session_id = optional_values['ussdSessionId'] if optional_values['to_addr'] is not None: session_event = TransportUserMessage.SESSION_RESUME to_addr = optional_values['to_addr'] content = optional_values['request'] else: session_event = TransportUserMessage.SESSION_NEW to_addr = optional_values['request'] content = None log.info( 'AatUssdTransport receiving inbound message from %s to %s.' % ( from_addr, to_addr)) yield self.publish_message( message_id=message_id, content=content, to_addr=to_addr, from_addr=from_addr, session_event=session_event, transport_type=self.transport_type, transport_metadata={ 'aat_ussd': { 'provider': provider, 'ussd_session_id': ussd_session_id, } }, provider=provider, )
def handle_event(self, event, handler_config): """ From an account and conversation, this finds a batch and a tag. While there could be multiple batches per conversation and multiple tags per batch, this assumes lists of one, and takes the first entry of each list TODO: Turn this into a generic API Command Sender? An Example of a event_dispatcher.yaml config file, with mapped conversations in the config: transport_name: event_dispatcher event_handlers: sign_up_handler: go.vumitools.handler.SendMessageCommandHandler account_handler_configs: '73ad76ec8c2e40858dc9d6b934049d95': - - ['a6a20571e77f4aa89a8b10a771b005bc', sign_up] - - [ sign_up_handler, { worker_name: 'multi_survey_application', conversation_key: 'a6a20571e77f4aa89a8b10a771b005bc' } ] """ log.info( "SendMessageCommandHandler handling event: %s with config: %s" % ( event, handler_config)) user_api = self.get_user_api(event.payload['account_key']) conv = yield user_api.conversation_store.get_conversation_by_key( event.payload['conversation_key']) conv = user_api.wrap_conversation(conv) command_data = event.payload['content'] command_data['batch_id'] = conv.batch.key command_data['msg_options'] = { 'helper_metadata': { 'go': {'user_account': event.payload['account_key']}, }, } sm_cmd = VumiApiCommand.command( handler_config['worker_name'], "send_message", command_data=command_data, conversation_key=handler_config['conversation_key'], account_key=event.payload['account_key'] ) log.info("Publishing command: %s" % sm_cmd) self.dispatcher.api_command_publisher.publish_message(sm_cmd)
def handle_send_to_tag(self, api, command): """ Sends a message to a specified tag. Command fields: - ``content``: The body of the reply message. - ``to_addr``: The address of the recipient (e.g. an MSISDN). - ``tagpool``: The name of the tagpool to send the message via. - ``tag``: The name of the tag (within the tagpool) to send the message from. Your Go user account must have the tag acquired. Reply fields: - ``success``: ``true`` if the operation was successful, otherwise ``false``. Example: .. code-block:: javascript api.request( 'outbound.send_to_tag', {content: 'Welcome!', to_addr: '+27831234567', tagpool: 'vumi_long', tag: 'default10001'}, function(reply) { api.log_info('Message sent: ' + reply.success); }); """ tag = (command.get('tagpool'), command.get('tag')) content = command.get('content') to_addr = command.get('to_addr') if any(not isinstance(u, unicode) for u in (tag[0], tag[1], content, to_addr)): returnValue(self._mkfail( command, reason="Tag, content or to_addr not specified")) log.info("Sending outbound message to %r via tag %r, content: %r" % (to_addr, tag, content)) conv = self.app_worker.conversation_for_api(api) tags = [tuple(endpoint.split(":", 1)) for endpoint in conv.extra_endpoints] if tag not in tags: returnValue(self._mkfail( command, reason="Tag %r not held by account" % (tag,))) msg_options = {} self.app_worker.add_conv_to_msg_options(conv, msg_options) endpoint = ':'.join(tag) yield self.app_worker.send_to( to_addr, content, endpoint=endpoint, **msg_options) returnValue(self.reply(command, success=True))
def create_wechat_menu(self, access_token, menu_structure): url = self.make_url('menu/create', {'access_token': access_token}) response = yield self.http_request_full( url, method='POST', data=json.dumps(menu_structure), headers={'Content-Type': ['application/json']}) if not http_ok(response): raise WeChatApiException( 'Received HTTP code: %r when creating the menu.' % ( response.code,)) data = json.loads(response.delivered_body) if data['errcode'] != 0: raise WeChatApiException( 'Received errcode: %(errcode)s, errmsg: %(errmsg)s ' 'when creating WeChat Menu.' % data) log.info('WeChat Menu created succesfully.')
def handle_raw_inbound_message(self, correlator, linkid, inbound_message): """ Handle incoming text messages from `SmsNotificationService` callbacks. """ log.info('Receiving SMS via ParlayX: %r: %r' % ( correlator, inbound_message,)) message_id = extract_message_id(correlator) return self.publish_message( message_id=message_id, content=inbound_message.message, to_addr=inbound_message.service_activation_number, from_addr=inbound_message.sender_address, provider='parlayx', transport_type=self.transport_type, transport_metadata=dict(linkid=linkid))
def process_command_stop(self, user_account_key, router_key): log.info("Stopping router '%s' for user '%s'." % (router_key, user_account_key)) router = yield self.get_router(user_account_key, router_key) if router is None: log.warning("Trying to stop missing router '%s' for user '%s'." % (router_key, user_account_key)) return if not router.stopping(): log.warning( "Trying to stop router '%s' for user '%s' with invalid " "status: %s" % (router_key, user_account_key, router.status)) return router.set_status_stopped() yield router.save()
def handle_outbound_message(self, message): """ Send a text message via the ParlayX client. """ log.info('Sending SMS via ParlayX: %r' % (message.to_json(),)) transport_metadata = message.get('transport_metadata', {}) d = self._parlayx_client.send_sms( message['to_addr'], message['content'], unique_correlator(message['message_id']), transport_metadata.get('linkid')) d.addErrback(self.handle_outbound_message_failure, message) d.addCallback( lambda requestIdentifier: self.publish_ack( message['message_id'], requestIdentifier)) return d
def process_command_stop(self, user_account_key, router_key): log.info("Stopping router '%s' for user '%s'." % ( router_key, user_account_key)) router = yield self.get_router(user_account_key, router_key) if router is None: log.warning( "Trying to stop missing router '%s' for user '%s'." % ( router_key, user_account_key)) return if not router.stopping(): log.warning( "Trying to stop router '%s' for user '%s' with invalid " "status: %s" % (router_key, user_account_key, router.status)) return router.set_status_stopped() yield router.save()
def create_wechat_menu(self, access_token, menu_structure): url = self.make_url('menu/create', {'access_token': access_token}) response = yield self.http_request_full( url, method='POST', data=json.dumps(menu_structure), headers={'Content-Type': ['application/json']}) if not http_ok(response): raise WeChatApiException( 'Received HTTP code: %r when creating the menu.' % (response.code, )) data = json.loads(response.delivered_body) if data['errcode'] != 0: raise WeChatApiException( 'Received errcode: %(errcode)s, errmsg: %(errmsg)s ' 'when creating WeChat Menu.' % data) log.info('WeChat Menu created succesfully.')
def process_command_start(self, user_account_key, conversation_key): log.info("Starting conversation '%s' for user '%s'." % (conversation_key, user_account_key)) conv = yield self.get_conversation(user_account_key, conversation_key) if conv is None: log.warning( "Trying to start missing conversation '%s' for user '%s'." % (conversation_key, user_account_key)) return if not conv.starting(): status = conv.get_status() log.warning( "Trying to start conversation '%s' for user '%s' with invalid " "status: %s" % (conversation_key, user_account_key, status)) return conv.set_status_started() yield conv.save()
def process_command_start(self, user_account_key, conversation_key): log.info("Starting conversation '%s' for user '%s'." % ( conversation_key, user_account_key)) conv = yield self.get_conversation(user_account_key, conversation_key) if conv is None: log.warning( "Trying to start missing conversation '%s' for user '%s'." % ( conversation_key, user_account_key)) return if not conv.starting(): status = conv.get_status() log.warning( "Trying to start conversation '%s' for user '%s' with invalid " "status: %s" % (conversation_key, user_account_key, status)) return conv.set_status_started() yield conv.save()
def _call_rapidsms(self, message): config = yield self.get_config(message) http_method = config.rapidsms_http_method.encode("utf-8") headers = self.get_auth_headers(config) yield self._store_message(message, config.vumi_reply_timeout) response = http_request_full(config.rapidsms_url.geturl(), message.to_json(), headers, http_method) response.addCallback(lambda response: log.info(response.code)) response.addErrback(lambda failure: log.err(failure)) yield response
def handle_outbound_message(self, message): # Errors message_id = message['message_id'] if not message['content']: yield self.publish_nack(message_id, self.NO_CONTENT_ERROR) return if not message['in_reply_to']: yield self.publish_nack(message_id, self.NOT_REPLY_ERROR) return # Generate outbound message body = self.generate_body( message['to_addr'], # msisdn message['transport_metadata']['sessionid'], # sessionid message['transport_metadata']['appid'], # appid message['in_reply_to'], message['content'], message['session_event'] ) log.info('BlastSMSUssdTransport outbound message with content: %r' % (body,)) # Finish Request response_id = self.finish_request( message['in_reply_to'], body, ) # Response failure if response_id is None: # we don't yield on this publish because if a message store is # used, that causes the worker to wait for Riak before processing # the message and responding to USSD messages is time critical. self.publish_nack(message_id, self.RESPONSE_FAILURE_ERROR) return # we don't yield on this publish because if a message store is # used, that causes the worker to wait for Riak before processing # the message and responding to USSD messages is time critical. self.publish_ack( user_message_id=message_id, sent_message_id=message_id)
def handle_event(self, event, handler_config): """ Expects the event 'content' to be a dict with the following keys and values: NOTE: perhaps all we need here is the message_id and lookup the rest of the required data from the message in the message store :param from_addr: the from_addr of the user interaction that triggered the event :param message_id: the id of the message that triggered the event, used to keep an audit trail of opt-outs and which messages they were triggered by. :param transport_type: the transport_type that the message was received on. """ account_key = event.payload['account_key'] oo_store = OptOutStore(self.vumi_api.manager, account_key) event_data = event.payload['content'] from_addr = event_data['from_addr'] message_id = event_data['message_id'] transport_type = event_data.get('transport_type') if transport_type != 'ussd': log.info("SNAUSSDOptOutHandler skipping non-ussd" " message for %r" % (from_addr,)) return contact = yield self.find_contact(account_key, from_addr) if contact: opted_out = contact.extra['opted_out'] if opted_out is not None and opted_out.isdigit(): if int(opted_out) > 1: yield oo_store.new_opt_out('msisdn', from_addr, { 'message_id': message_id, }) else: yield oo_store.delete_opt_out('msisdn', from_addr)
def populate_conversation_buckets(self): account_keys = yield self.find_account_keys() num_conversations = 0 # We deliberarely serialise this. We don't want to hit the datastore # too hard for metrics. for account_key in account_keys: conv_keys = yield self.find_conversations_for_account(account_key) num_conversations += len(conv_keys) for conv_key in conv_keys: bucket = self.bucket_for_conversation(conv_key) if conv_key not in self._conversation_workers: # TODO: Clear out archived conversations user_api = self.vumi_api.get_user_api(account_key) conv = yield user_api.get_wrapped_conversation(conv_key) self._conversation_workers[conv_key] = conv.worker_name worker_name = self._conversation_workers[conv_key] self._buckets[bucket].append( (account_key, conv_key, worker_name)) log.info( "Scheduled metrics commands for %d conversations in %d accounts." % (num_conversations, len(account_keys)))
def handle_event(self, event, handler_config): """ Hit the Yo payment gateway when a vxpoll is completed. Expects 'content' to be a dict with the following keys and values: :param from_addr: The address from which the message was received and which should be topped up with airtime. """ participant = event['content']['participant'] interactions = int(participant['interactions']) amount = interactions * int(self.amount) request_params = { 'msisdn': event['content']['from_addr'], 'amount': amount, 'reason': self.reason, } headers = self.get_auth_headers(self.username, self.password) headers.update({ 'Content-Type': 'application/x-www-form-urlencoded', }) log.info('Sending %s to %s via HTTP %s' % ( request_params, self.url, self.method, )) response = yield http_request_full(self.url, data=urlencode(request_params), headers=headers, method=self.method) log.info('Received response: %s / %s' % ( response.code, response.delivered_body, ))
def get_conversation_fallback(self, account_key_or_batch_id, conv_key): # HACK: If we can't find a conversation, we might have an old entry # that uses a batch_id. conv = yield self.get_conversation(account_key_or_batch_id, conv_key) if conv is None: log.info("Trying to find conversation '%s' by batch_id." % ( conv_key,)) batch = yield self.vumi_api.mdb.get_batch(account_key_or_batch_id) if batch is None: log.warning('Cannot find batch for batch_id %s' % ( account_key_or_batch_id,)) return user_account_key = batch.metadata["user_account"] if user_account_key is None: log.warning("No account key in batch metadata: %r" % (batch,)) return yield self.redis.srem('scheduled_conversations', json.dumps( [account_key_or_batch_id, conv_key])) yield self.redis.sadd('scheduled_conversations', json.dumps( [user_account_key, conv_key])) conv = yield self.get_conversation(user_account_key, conv_key) returnValue(conv)
def handle_event(self, event, handler_config): """ Hit the Yo payment gateway when a vxpoll is completed. Expects 'content' to be a dict with the following keys and values: :param from_addr: The address from which the message was received and which should be topped up with airtime. """ participant = event['content']['participant'] interactions = int(participant['interactions']) amount = interactions * int(self.amount) request_params = { 'msisdn': event['content']['from_addr'], 'amount': amount, 'reason': self.reason, } headers = self.get_auth_headers(self.username, self.password) headers.update({ 'Content-Type': 'application/x-www-form-urlencoded', }) log.info('Sending %s to %s via HTTP %s' % ( request_params, self.url, self.method, )) response = yield http_request_full(self.url, data=urlencode(request_params), headers=headers, method=self.method) log.info('Received response: %s / %s' % (response.code, response.delivered_body,))
def get_conversation_fallback(self, account_key_or_batch_id, conv_key): # HACK: If we can't find a conversation, we might have an old entry # that uses a batch_id. conv = yield self.get_conversation(account_key_or_batch_id, conv_key) if conv is None: log.info("Trying to find conversation '%s' by batch_id." % (conv_key, )) batch = yield self.vumi_api.mdb.get_batch(account_key_or_batch_id) if batch is None: log.warning('Cannot find batch for batch_id %s' % (account_key_or_batch_id, )) return user_account_key = batch.metadata["user_account"] if user_account_key is None: log.warning("No account key in batch metadata: %r" % (batch, )) return yield self.redis.srem( 'scheduled_conversations', json.dumps([account_key_or_batch_id, conv_key])) yield self.redis.sadd('scheduled_conversations', json.dumps([user_account_key, conv_key])) conv = yield self.get_conversation(user_account_key, conv_key) returnValue(conv)
def handle_event(self, event, handler_config): """Set the appropriate subscription flag on a contact object. The event should have the following fields: * ``campaign_name``: Name of the campaign to manage subscription to. * ``operation``: Must be ``subscribe`` or ``unsubscribe``. * ``contact_id``: Contact id to operate on. An Example of a event_dispatcher.yaml config file, with mapped conversations in the config: transport_name: event_dispatcher event_handlers: subscription_handler: go.vumitools.subscription.handlers.SubscriptionHandler account_handler_configs: '73ad76ec8c2e40858dc9d6b934049d95': - - ['a6a20571e77f4aa89a8b10a771b005bc', subscribe] - - [subscription_handler, {}] """ log.info( "SubscriptionHandler handling event: %s with config: %s" % ( event, handler_config)) user_api = self.get_user_api(event.payload['account_key']) fields = event.payload['content'] contact = yield user_api.contact_store.get_contact_by_key( fields['contact_id']) contact.subscription[fields['campaign_name']] = { 'subscribe': u'subscribed', 'unsubscribe': u'unsubscribed', }[fields['operation']] yield contact.save()
def handle_event(self, event, handler_config): """Set the appropriate subscription flag on a contact object. The event should have the following fields: * ``campaign_name``: Name of the campaign to manage subscription to. * ``operation``: Must be ``subscribe`` or ``unsubscribe``. * ``contact_id``: Contact id to operate on. An Example of a event_dispatcher.yaml config file, with mapped conversations in the config: transport_name: event_dispatcher event_handlers: subscription_handler: go.vumitools.subscription.handlers.SubscriptionHandler account_handler_configs: '73ad76ec8c2e40858dc9d6b934049d95': - - ['a6a20571e77f4aa89a8b10a771b005bc', subscribe] - - [subscription_handler, {}] """ log.info("SubscriptionHandler handling event: %s with config: %s" % (event, handler_config)) user_api = self.get_user_api(event.payload['account_key']) fields = event.payload['content'] contact = yield user_api.contact_store.get_contact_by_key( fields['contact_id']) contact.subscription[fields['campaign_name']] = { 'subscribe': u'subscribed', 'unsubscribe': u'unsubscribed', }[fields['operation']] yield contact.save()
def consume_ack(self, event): log.info("Acknowledgement received for message %r" % (event['user_message_id']))
def on_window_cleanup(self, window_id): log.info('Finished window %s, removing.' % (window_id,))
def teardown_transport(self): config = self.get_static_config() log.info('Stopping ParlayX transport: %s' % (self.transport_name,)) yield self.web_resource.loseConnection() if config.start_notifications: yield self._parlayx_client.stop_sms_notification()
def handle_send_to_endpoint(self, api, command): """ Sends a message to a specified endpoint. Command fields: - ``content``: The body of the reply message. - ``to_addr``: The address of the recipient (e.g. an MSISDN). - ``endpoint``: The name of the endpoint to send the message via. Reply fields: - ``success``: ``true`` if the operation was successful, otherwise ``false``. Example: .. code-block:: javascript api.request( 'outbound.send_to_endpoint', {content: 'Welcome!', to_addr: '+27831234567', endpoint: 'sms'}, function(reply) { api.log_info('Message sent: ' + reply.success); }); """ if not 'content' in command: return self._mkfaild(command, reason=u"'content' must be given in sends.") if not isinstance(command['content'], (unicode, type(None))): return self._mkfaild(command, reason=u"'content' must be unicode or null.") if not isinstance(command.get('endpoint'), unicode): return self._mkfaild(command, reason=u"'endpoint' must be given in sends.") if not isinstance(command.get('to_addr'), unicode): return self._mkfaild(command, reason=u"'to_addr' must be given in sends.") endpoint = command['endpoint'] content = command['content'] to_addr = command['to_addr'] conv = self.app_worker.conversation_for_api(api) if endpoint not in conv.extra_endpoints: return self._mkfaild(command, reason="Endpoint %r not configured" % (endpoint, )) msg_options = {} self.app_worker.add_conv_to_msg_options(conv, msg_options) log.info( "Sending outbound message to %r via endpoint %r, content: %r" % (to_addr, endpoint, content)) d = self.app_worker.send_to(to_addr, content, endpoint=endpoint, **msg_options) d.addCallback(lambda r: self.reply(command, success=True)) d.addErrback( lambda f: self._mkfail(command, unicode(f.getErrorMessage()))) return d
def consume_delivery_report(self, event): log.info("Delivery report received for message %r, status %r" % (event['user_message_id'], event['delivery_status']))
def on_window_cleanup(self, window_id): log.info('Finished window %s, removing.' % (window_id, ))
def test_system_filtering(self): lc = LogCatcher(system="^ab") with lc: log.info("Test 1", system="abc") log.info("Test 2", system="def") self.assertEqual(lc.messages(), ["Test 1"])
def test_message_filtering(self): lc = LogCatcher(message="^Keep") with lc: log.info("Keep this") log.info("Discard this") self.assertEqual(lc.messages(), ["Keep this"])
def send_event_to_client(self, event, conversation, push_url): if push_url is None: log.info("push_event_url not configured for conversation: %s" % (conversation.key)) return return self.push(push_url, event)
def test_message_concatenation(self): lc = LogCatcher() with lc: log.info("Part 1", "Part 2") self.assertEqual(lc.messages(), ["Part 1 Part 2"])