예제 #1
0
 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)
예제 #2
0
    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)
예제 #3
0
    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'])
예제 #4
0
파일: test_api.py 프로젝트: TouK/vumi-go
    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'])
예제 #5
0
 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)
예제 #6
0
 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)
예제 #7
0
 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)
예제 #8
0
파일: api.py 프로젝트: TouK/vumi-go
 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)
예제 #9
0
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:
예제 #10
0
    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()))
예제 #11
0
파일: api.py 프로젝트: TouK/vumi-go
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: