def find_contact(self, account_key, msisdn): contact_store = ContactStore( self.dispatcher.vumi_api.manager, account_key) try: contact = yield contact_store.contact_for_addr('ussd', msisdn) returnValue(contact) except ContactError: returnValue(None)
def setUp(self): self.vumi_helper = yield self.add_helper(VumiApiHelper()) self.user_helper = yield self.vumi_helper.make_user(u'user') user_account = yield self.user_helper.get_user_account() self.store = ContactStore.from_user_account(user_account) self.alt_user_helper = yield self.vumi_helper.make_user(u'other_user') alt_user_account = yield self.alt_user_helper.get_user_account() self.store_alt = ContactStore.from_user_account(alt_user_account)
def test_optout_filtering(self): group = yield self.user_api.contact_store.new_group(u'test-group') user_account = yield self.user_helper.get_user_account() optout_store = OptOutStore.from_user_account(user_account) contact_store = ContactStore.from_user_account(user_account) # Create two random contacts yield self.user_api.contact_store.new_contact(msisdn=u'+27761234567', groups=[group.key]) yield self.user_api.contact_store.new_contact(msisdn=u'+27760000000', groups=[group.key]) conv = yield self.user_helper.create_conversation( u'dummy', delivery_class=u'sms', groups=[group]) # Opt out the first contact yield optout_store.new_opt_out(u'msisdn', u'+27761234567', {'message_id': u'the-message-id'}) contact_keys = yield contact_store.get_contacts_for_conversation(conv) all_addrs = [] for contacts in contact_store.contacts.load_all_bunches(contact_keys): for contact in (yield contacts): all_addrs.append(contact.addr_for(conv.delivery_class)) self.assertEqual(set(all_addrs), set(['+27760000000', '+27761234567'])) optedin_addrs = [] for contacts in (yield conv.get_opted_in_contact_bunches( conv.delivery_class)): for contact in (yield contacts): optedin_addrs.append(contact.addr_for(conv.delivery_class)) self.assertEqual(optedin_addrs, ['+27760000000'])
def test_optout_filtering(self): group = yield self.user_api.contact_store.new_group(u'test-group') user_account = yield self.user_helper.get_user_account() optout_store = OptOutStore.from_user_account(user_account) contact_store = ContactStore.from_user_account(user_account) # Create two random contacts yield self.user_api.contact_store.new_contact( msisdn=u'+27761234567', groups=[group.key]) yield self.user_api.contact_store.new_contact( msisdn=u'+27760000000', groups=[group.key]) conv = yield self.user_helper.create_conversation( u'dummy', delivery_class=u'sms', groups=[group]) # Opt out the first contact yield optout_store.new_opt_out(u'msisdn', u'+27761234567', { 'message_id': u'the-message-id' }) contact_keys = yield contact_store.get_contacts_for_conversation(conv) all_addrs = [] for contacts in contact_store.contacts.load_all_bunches(contact_keys): for contact in (yield contacts): all_addrs.append(contact.addr_for(conv.delivery_class)) self.assertEqual(set(all_addrs), set(['+27760000000', '+27761234567'])) optedin_addrs = [] for contacts in (yield conv.get_opted_in_contact_bunches( conv.delivery_class)): for contact in (yield contacts): optedin_addrs.append(contact.addr_for(conv.delivery_class)) self.assertEqual(optedin_addrs, ['+27760000000'])
def setUp(self): self.vumi_helper = yield self.add_helper( VumiApiHelper(is_sync=self.is_sync)) self.user_helper = yield self.vumi_helper.make_user(u'user') user_account = yield self.user_helper.get_user_account() self.optout_store = OptOutStore.from_user_account(user_account) self.conv_store = ConversationStore.from_user_account(user_account) self.contact_store = ContactStore.from_user_account(user_account)
def __init__(self, api, user_account_key): # We could get either bytes or unicode here. Decode if necessary. if not isinstance(user_account_key, unicode): user_account_key = user_account_key.decode('utf8') self.api = api self.manager = self.api.manager self.user_account_key = user_account_key self.conversation_store = ConversationStore(self.api.manager, self.user_account_key) self.contact_store = ContactStore(self.api.manager, self.user_account_key) self.router_store = RouterStore(self.api.manager, self.user_account_key) self.channel_store = ChannelStore(self.api.manager, self.user_account_key) self.optout_store = OptOutStore(self.api.manager, self.user_account_key)
class VumiUserApi(object): conversation_wrapper = ConversationWrapper def __init__(self, api, user_account_key): # We could get either bytes or unicode here. Decode if necessary. if not isinstance(user_account_key, unicode): user_account_key = user_account_key.decode('utf8') self.api = api self.manager = self.api.manager self.user_account_key = user_account_key self.conversation_store = ConversationStore(self.api.manager, self.user_account_key) self.contact_store = ContactStore(self.api.manager, self.user_account_key) self.router_store = RouterStore(self.api.manager, self.user_account_key) self.channel_store = ChannelStore(self.api.manager, self.user_account_key) self.optout_store = OptOutStore(self.api.manager, self.user_account_key) def exists(self): return self.api.user_exists(self.user_account_key) @classmethod def from_config_sync(cls, user_account_key, config): return cls(VumiApi.from_config_sync(config), user_account_key) @classmethod def from_config_async(cls, user_account_key, config): d = VumiApi.from_config_async(config) return d.addCallback(cls, user_account_key) def get_user_account(self): return self.api.get_user_account(self.user_account_key) def wrap_conversation(self, conversation): """Wrap a conversation with a ConversationWrapper. What it says on the tin, really. :param Conversation conversation: Conversation object to wrap. :rtype: ConversationWrapper. """ return self.conversation_wrapper(conversation, self) @Manager.calls_manager def get_wrapped_conversation(self, conversation_key): conversation = yield self.conversation_store.get_conversation_by_key( conversation_key) if conversation: returnValue(self.wrap_conversation(conversation)) def get_conversation(self, conversation_key): return self.conversation_store.get_conversation_by_key( conversation_key) def get_router(self, router_key): return self.router_store.get_router_by_key(router_key) @Manager.calls_manager def get_channel(self, tag): tagpool_meta = yield self.api.tpm.get_metadata(tag[0]) tag_info = yield self.api.mdb.get_tag_info(tag) channel = yield self.channel_store.get_channel_by_tag( tag, tagpool_meta, tag_info.current_batch.key) returnValue(channel) @Manager.calls_manager def finished_conversations(self): conv_store = self.conversation_store keys = yield conv_store.list_conversations() conversations = [] for bunch in conv_store.conversations.load_all_bunches(keys): conversations.extend((yield bunch)) returnValue([c for c in conversations if c.ended()]) @Manager.calls_manager def active_conversations(self): keys = yield self.conversation_store.list_active_conversations() # NOTE: This assumes that we don't have very large numbers of active # conversations. convs = [] for convs_bunch in self.conversation_store.load_all_bunches(keys): convs.extend((yield convs_bunch)) returnValue(convs) @Manager.calls_manager def running_conversations(self): keys = yield self.conversation_store.list_running_conversations() # NOTE: This assumes that we don't have very large numbers of active # conversations. convs = [] for convs_bunch in self.conversation_store.load_all_bunches(keys): convs.extend((yield convs_bunch)) returnValue(convs) @Manager.calls_manager def draft_conversations(self): # TODO: Get rid of this once the old UI finally goes away. conversations = yield self.active_conversations() returnValue([c for c in conversations if c.is_draft()]) @Manager.calls_manager def active_routers(self): keys = yield self.router_store.list_active_routers() # NOTE: This assumes that we don't have very large numbers of active # routers. routers = [] for routers_bunch in self.router_store.load_all_bunches(keys): routers.extend((yield routers_bunch)) returnValue(routers) @Manager.calls_manager def active_channels(self): channels = [] user_account = yield self.get_user_account() for tag in user_account.tags: channel = yield self.get_channel(tuple(tag)) channels.append(channel) returnValue(channels) @Manager.calls_manager def tagpools(self): user_account = yield self.get_user_account() tp_usage = defaultdict(int) for tag in user_account.tags: tp_usage[tag[0]] += 1 all_pools = yield self.api.tpm.list_pools() allowed_pools = set() for tp_bunch in user_account.tagpools.load_all_bunches(): for tp in (yield tp_bunch): if (tp.max_keys is None or tp.max_keys > tp_usage[tp.tagpool]): allowed_pools.add(tp.tagpool) available_pools = [] for pool in all_pools: if pool not in allowed_pools: continue free_tags = yield self.api.tpm.free_tags(pool) if free_tags: available_pools.append(pool) pool_data = dict([(pool, (yield self.api.tpm.get_metadata(pool))) for pool in available_pools]) returnValue(TagpoolSet(pool_data)) @Manager.calls_manager def applications(self): user_account = yield self.get_user_account() # NOTE: This assumes that we don't have very large numbers of # applications. app_permissions = [] for permissions in user_account.applications.load_all_bunches(): app_permissions.extend((yield permissions)) applications = [ permission.application for permission in app_permissions ] app_settings = configured_conversations() returnValue( SortedDict([(application, app_settings[application]) for application in sorted(applications) if application in app_settings])) @Manager.calls_manager def router_types(self): # TODO: Permissions. yield None router_settings = configured_routers() returnValue( SortedDict([(router_type, router_settings[router_type]) for router_type in sorted(router_settings)])) def list_groups(self): return self.contact_store.list_groups() @Manager.calls_manager def new_conversation(self, conversation_type, name, description, config, batch_id=None, **fields): if not batch_id: batch_id = yield self.api.mdb.batch_start( tags=[], user_account=self.user_account_key) conv = yield self.conversation_store.new_conversation( conversation_type, name, description, config, batch_id, **fields) returnValue(conv) @Manager.calls_manager def new_router(self, router_type, name, description, config, batch_id=None, **fields): if not batch_id: batch_id = yield self.api.mdb.batch_start( tags=[], user_account=self.user_account_key) router = yield self.router_store.new_router(router_type, name, description, config, batch_id, **fields) returnValue(router) @Manager.calls_manager def get_routing_table(self, user_account=None): if user_account is None: user_account = yield self.get_user_account() if user_account.routing_table is None: raise VumiError("Routing table missing for account: %s" % (user_account.key, )) returnValue(user_account.routing_table) @Manager.calls_manager def validate_routing_table(self, user_account=None): """Check that the routing table on this account is valid. Currently we just check account ownership of tags and conversations. TODO: Cycle detection, if that's even possible. Maybe other stuff. TODO: Determine if this is necessary and move it elsewhere if it is. """ if user_account is None: user_account = yield self.get_user_account() routing_table = yield self.get_routing_table(user_account) # We don't care about endpoints here, only connectors. routing_connectors = set() for src_conn, _src_ep, dst_conn, _dst_ep in routing_table.entries(): routing_connectors.add(src_conn) routing_connectors.add(dst_conn) # Checking tags is cheap and easy, so do that first. channels = yield self.active_channels() for channel in channels: channel_conn = channel.get_connector() if channel_conn in routing_connectors: routing_connectors.remove(channel_conn) # Now we run through active conversations to check those. convs = yield self.active_conversations() for conv in convs: conv_conn = conv.get_connector() if conv_conn in routing_connectors: routing_connectors.remove(conv_conn) if routing_connectors: raise VumiError( "Routing table contains illegal connector names: %s" % (routing_connectors, )) @Manager.calls_manager def _update_tag_data_for_acquire(self, user_account, tag): # The batch we create here gets added to the tag_info and we can fish # it out later. When we replace this with proper channel objects we can # stash it there like we do with conversations and routers. yield self.api.mdb.batch_start([tag], user_account=user_account.key) user_account.tags.append(tag) tag_info = yield self.api.mdb.get_tag_info(tag) tag_info.metadata['user_account'] = user_account.key.decode('utf-8') yield tag_info.save() yield user_account.save() @Manager.calls_manager def acquire_tag(self, pool): """Acquire a tag from a given tag pool. Tags should be held for the duration of a conversation. :type pool: str :param pool: name of the pool to retrieve tags from. :rtype: The tag acquired or None if no tag was available. """ user_account = yield self.get_user_account() if not (yield user_account.has_tagpool_permission(pool)): log.warning("Account '%s' trying to access forbidden pool '%s'" % (user_account.key, pool)) returnValue(None) tag = yield self.api.tpm.acquire_tag(pool) if tag is not None: yield self._update_tag_data_for_acquire(user_account, tag) returnValue(tag) @Manager.calls_manager def acquire_specific_tag(self, tag): """Acquire a specific tag. Tags should be held for the duration of a conversation. :type tag: tag tuple :param tag: The tag to acquire. :rtype: The tag acquired or None if the tag was not available. """ user_account = yield self.get_user_account() if not (yield user_account.has_tagpool_permission(tag[0])): log.warning("Account '%s' trying to access forbidden pool '%s'" % (user_account.key, tag[0])) returnValue(None) tag = yield self.api.tpm.acquire_specific_tag(tag) if tag is not None: yield self._update_tag_data_for_acquire(user_account, tag) returnValue(tag) @Manager.calls_manager def release_tag(self, tag): """Release a tag back to the pool it came from. Tags should be released only once a conversation is finished. :type pool: str :param pool: name of the pool to return the tag too (must be the same as the name of the pool the tag came from). :rtype: None. """ user_account = yield self.get_user_account() try: user_account.tags.remove(list(tag)) except ValueError, e: log.error("Tag not allocated to account: %s" % (tag, ), e) else:
def handle_save(self, api, command): """ Saves a contact's data, overwriting the contact's previous data. Use with care. This operation only works for existing contacts. For creating new contacts, use :func:`handle_new`. Command fields: - ``contact``: The contact's data. **Note**: ``key`` must be a field in the contact data in order identify the contact. Success reply fields: - ``success``: set to ``true`` - ``contact``: An object containing the contact's data. Failure reply fields: - ``success``: set to ``false`` - ``reason``: Reason for the failure Example: .. code-block:: javascript api.request( 'contacts.save', { contact: { 'key': 'f953710a2472447591bd59e906dc2c26', 'surname': 'Person', 'user_account': 'test-0-user', 'msisdn': '+27831234567', 'groups': ['group-a', 'group-b'], 'name': 'A Random' } }, function(reply) { api.log_info(reply.success); }); """ try: if not isinstance(command.get('contact'), dict): raise SandboxError( "'contact' needs to be specified and be a dict of field " "name-value pairs") fields = command['contact'] if not isinstance(fields.get('key'), unicode): raise SandboxError( "'key' needs to be specified as a field in 'contact' and " "be a unicode string") # These are foreign keys. groups = fields.pop('groups', []) key = fields.pop('key') contact_store = self._contact_store_for_api(api) # raise an exception if the contact does not exist yield contact_store.get_contact_by_key(key) contact = contact_store.contacts( key, user_account=contact_store.user_account_key, **ContactStore.settable_contact_fields(**fields)) # since we are basically creating a 'new' contact with the same # key, we can be sure that the old groups were removed for group in groups: contact.add_to_group(group) yield contact.save() except (SandboxError, ContactError) as e: returnValue(self.reply(command, success=False, reason=unicode(e))) returnValue(self.reply( command, success=True, contact=contact.get_data()))
class VumiUserApi(object): conversation_wrapper = ConversationWrapper def __init__(self, api, user_account_key): # We could get either bytes or unicode here. Decode if necessary. if not isinstance(user_account_key, unicode): user_account_key = user_account_key.decode('utf8') self.api = api self.manager = self.api.manager self.user_account_key = user_account_key self.conversation_store = ConversationStore(self.api.manager, self.user_account_key) self.contact_store = ContactStore(self.api.manager, self.user_account_key) self.router_store = RouterStore(self.api.manager, self.user_account_key) self.channel_store = ChannelStore(self.api.manager, self.user_account_key) self.optout_store = OptOutStore(self.api.manager, self.user_account_key) def exists(self): return self.api.user_exists(self.user_account_key) @classmethod def from_config_sync(cls, user_account_key, config): return cls(VumiApi.from_config_sync(config), user_account_key) @classmethod def from_config_async(cls, user_account_key, config): d = VumiApi.from_config_async(config) return d.addCallback(cls, user_account_key) def get_user_account(self): return self.api.get_user_account(self.user_account_key) def wrap_conversation(self, conversation): """Wrap a conversation with a ConversationWrapper. What it says on the tin, really. :param Conversation conversation: Conversation object to wrap. :rtype: ConversationWrapper. """ return self.conversation_wrapper(conversation, self) @Manager.calls_manager def get_wrapped_conversation(self, conversation_key): conversation = yield self.conversation_store.get_conversation_by_key( conversation_key) if conversation: returnValue(self.wrap_conversation(conversation)) def get_conversation(self, conversation_key): return self.conversation_store.get_conversation_by_key( conversation_key) def get_router(self, router_key): return self.router_store.get_router_by_key(router_key) @Manager.calls_manager def get_channel(self, tag): tagpool_meta = yield self.api.tpm.get_metadata(tag[0]) tag_info = yield self.api.mdb.get_tag_info(tag) channel = yield self.channel_store.get_channel_by_tag( tag, tagpool_meta, tag_info.current_batch.key) returnValue(channel) @Manager.calls_manager def finished_conversations(self): conv_store = self.conversation_store keys = yield conv_store.list_conversations() conversations = [] for bunch in conv_store.conversations.load_all_bunches(keys): conversations.extend((yield bunch)) returnValue([c for c in conversations if c.ended()]) @Manager.calls_manager def active_conversations(self): keys = yield self.conversation_store.list_active_conversations() # NOTE: This assumes that we don't have very large numbers of active # conversations. convs = [] for convs_bunch in self.conversation_store.load_all_bunches(keys): convs.extend((yield convs_bunch)) returnValue(convs) @Manager.calls_manager def running_conversations(self): keys = yield self.conversation_store.list_running_conversations() # NOTE: This assumes that we don't have very large numbers of active # conversations. convs = [] for convs_bunch in self.conversation_store.load_all_bunches(keys): convs.extend((yield convs_bunch)) returnValue(convs) @Manager.calls_manager def draft_conversations(self): # TODO: Get rid of this once the old UI finally goes away. conversations = yield self.active_conversations() returnValue([c for c in conversations if c.is_draft()]) @Manager.calls_manager def active_routers(self): keys = yield self.router_store.list_active_routers() # NOTE: This assumes that we don't have very large numbers of active # routers. routers = [] for routers_bunch in self.router_store.load_all_bunches(keys): routers.extend((yield routers_bunch)) returnValue(routers) @Manager.calls_manager def active_channels(self): channels = [] user_account = yield self.get_user_account() for tag in user_account.tags: channel = yield self.get_channel(tuple(tag)) channels.append(channel) returnValue(channels) @Manager.calls_manager def tagpools(self): user_account = yield self.get_user_account() tp_usage = defaultdict(int) for tag in user_account.tags: tp_usage[tag[0]] += 1 all_pools = yield self.api.tpm.list_pools() allowed_pools = set() for tp_bunch in user_account.tagpools.load_all_bunches(): for tp in (yield tp_bunch): if (tp.max_keys is None or tp.max_keys > tp_usage[tp.tagpool]): allowed_pools.add(tp.tagpool) available_pools = [] for pool in all_pools: if pool not in allowed_pools: continue free_tags = yield self.api.tpm.free_tags(pool) if free_tags: available_pools.append(pool) pool_data = dict([(pool, (yield self.api.tpm.get_metadata(pool))) for pool in available_pools]) returnValue(TagpoolSet(pool_data)) @Manager.calls_manager def applications(self): user_account = yield self.get_user_account() # NOTE: This assumes that we don't have very large numbers of # applications. app_permissions = [] for permissions in user_account.applications.load_all_bunches(): app_permissions.extend((yield permissions)) applications = [permission.application for permission in app_permissions] app_settings = configured_conversations() returnValue(SortedDict([(application, app_settings[application]) for application in sorted(applications) if application in app_settings])) @Manager.calls_manager def router_types(self): # TODO: Permissions. yield None router_settings = configured_routers() returnValue(SortedDict([(router_type, router_settings[router_type]) for router_type in sorted(router_settings)])) def list_groups(self): return self.contact_store.list_groups() @Manager.calls_manager def new_conversation(self, conversation_type, name, description, config, batch_id=None, **fields): if not batch_id: batch_id = yield self.api.mdb.batch_start( tags=[], user_account=self.user_account_key) conv = yield self.conversation_store.new_conversation( conversation_type, name, description, config, batch_id, **fields) returnValue(conv) @Manager.calls_manager def new_router(self, router_type, name, description, config, batch_id=None, **fields): if not batch_id: batch_id = yield self.api.mdb.batch_start( tags=[], user_account=self.user_account_key) router = yield self.router_store.new_router( router_type, name, description, config, batch_id, **fields) returnValue(router) @Manager.calls_manager def get_routing_table(self, user_account=None): if user_account is None: user_account = yield self.get_user_account() if user_account.routing_table is None: raise VumiError( "Routing table missing for account: %s" % (user_account.key,)) returnValue(user_account.routing_table) @Manager.calls_manager def validate_routing_table(self, user_account=None): """Check that the routing table on this account is valid. Currently we just check account ownership of tags and conversations. TODO: Cycle detection, if that's even possible. Maybe other stuff. TODO: Determine if this is necessary and move it elsewhere if it is. """ if user_account is None: user_account = yield self.get_user_account() routing_table = yield self.get_routing_table(user_account) # We don't care about endpoints here, only connectors. routing_connectors = set() for src_conn, _src_ep, dst_conn, _dst_ep in routing_table.entries(): routing_connectors.add(src_conn) routing_connectors.add(dst_conn) # Checking tags is cheap and easy, so do that first. channels = yield self.active_channels() for channel in channels: channel_conn = channel.get_connector() if channel_conn in routing_connectors: routing_connectors.remove(channel_conn) # Now we run through active conversations to check those. convs = yield self.active_conversations() for conv in convs: conv_conn = conv.get_connector() if conv_conn in routing_connectors: routing_connectors.remove(conv_conn) if routing_connectors: raise VumiError( "Routing table contains illegal connector names: %s" % ( routing_connectors,)) @Manager.calls_manager def _update_tag_data_for_acquire(self, user_account, tag): # The batch we create here gets added to the tag_info and we can fish # it out later. When we replace this with proper channel objects we can # stash it there like we do with conversations and routers. yield self.api.mdb.batch_start([tag], user_account=user_account.key) user_account.tags.append(tag) tag_info = yield self.api.mdb.get_tag_info(tag) tag_info.metadata['user_account'] = user_account.key.decode('utf-8') yield tag_info.save() yield user_account.save() @Manager.calls_manager def acquire_tag(self, pool): """Acquire a tag from a given tag pool. Tags should be held for the duration of a conversation. :type pool: str :param pool: name of the pool to retrieve tags from. :rtype: The tag acquired or None if no tag was available. """ user_account = yield self.get_user_account() if not (yield user_account.has_tagpool_permission(pool)): log.warning("Account '%s' trying to access forbidden pool '%s'" % ( user_account.key, pool)) returnValue(None) tag = yield self.api.tpm.acquire_tag(pool) if tag is not None: yield self._update_tag_data_for_acquire(user_account, tag) returnValue(tag) @Manager.calls_manager def acquire_specific_tag(self, tag): """Acquire a specific tag. Tags should be held for the duration of a conversation. :type tag: tag tuple :param tag: The tag to acquire. :rtype: The tag acquired or None if the tag was not available. """ user_account = yield self.get_user_account() if not (yield user_account.has_tagpool_permission(tag[0])): log.warning("Account '%s' trying to access forbidden pool '%s'" % ( user_account.key, tag[0])) returnValue(None) tag = yield self.api.tpm.acquire_specific_tag(tag) if tag is not None: yield self._update_tag_data_for_acquire(user_account, tag) returnValue(tag) @Manager.calls_manager def release_tag(self, tag): """Release a tag back to the pool it came from. Tags should be released only once a conversation is finished. :type pool: str :param pool: name of the pool to return the tag too (must be the same as the name of the pool the tag came from). :rtype: None. """ user_account = yield self.get_user_account() try: user_account.tags.remove(list(tag)) except ValueError, e: log.error("Tag not allocated to account: %s" % (tag,), e) else: