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:
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: