class JunebugApi(object): app = Klein() def __init__(self, service, config): self.service = service self.redis_config = config.redis self.amqp_config = config.amqp self.config = config @inlineCallbacks def setup(self, redis=None, message_sender=None): if redis is None: redis = yield TxRedisManager.from_config(self.redis_config) if message_sender is None: message_sender = MessageSender('amqp-spec-0-8.xml', self.amqp_config) self.redis = redis self.message_sender = message_sender self.message_sender.setServiceParent(self.service) self.inbounds = InboundMessageStore(self.redis, self.config.inbound_message_ttl) self.outbounds = OutboundMessageStore(self.redis, self.config.outbound_message_ttl) self.message_rate = MessageRateStore(self.redis) self.plugins = [] for plugin_config in self.config.plugins: cls = load_class_by_string(plugin_config['type']) plugin = cls() yield plugin.start_plugin(plugin_config, self.config) self.plugins.append(plugin) yield Channel.start_all_channels(self.redis, self.config, self.service, self.plugins) @inlineCallbacks def teardown(self): yield self.redis.close_manager() for plugin in self.plugins: yield plugin.stop_plugin() @app.handle_errors(JunebugError) def generic_junebug_error(self, request, failure): return response(request, failure.value.description, { 'errors': [{ 'type': failure.value.name, 'message': failure.getErrorMessage(), }] }, code=failure.value.code) @app.handle_errors(HTTPException) def http_error(self, request, failure): error = { 'code': failure.value.code, 'type': failure.value.name, 'message': failure.getErrorMessage(), } if getattr(failure.value, 'new_url', None) is not None: request.setHeader('Location', failure.value.new_url) error['new_url'] = failure.value.new_url return response(request, failure.value.description, { 'errors': [error], }, code=failure.value.code) @app.handle_errors def generic_error(self, request, failure): log.err(failure) return response(request, 'generic error', { 'errors': [{ 'type': failure.type.__name__, 'message': failure.getErrorMessage(), }] }, code=http.INTERNAL_SERVER_ERROR) @app.route('/channels/', methods=['GET']) @inlineCallbacks def get_channel_list(self, request): '''List all channels''' ids = yield Channel.get_all(self.redis) returnValue(response(request, 'channels listed', sorted(ids))) @app.route('/channels/', methods=['POST']) @json_body @validate( body_schema({ 'type': 'object', 'properties': { 'type': { 'type': 'string' }, 'label': { 'type': 'string' }, 'config': { 'type': 'object' }, 'metadata': { 'type': 'object' }, 'status_url': { 'type': 'string' }, 'mo_url': { 'type': 'string' }, 'mo_url_auth_token': { 'type': 'string' }, 'amqp_queue': { 'type': 'string' }, 'rate_limit_count': { 'type': 'integer', 'minimum': 0, }, 'rate_limit_window': { 'type': 'integer', 'minimum': 0, }, 'character_limit': { 'type': 'integer', 'minimum': 0, }, }, 'required': ['type', 'config'], })) @inlineCallbacks def create_channel(self, request, body): '''Create a channel''' if not (body.get('mo_url') or body.get('amqp_queue')): raise ApiUsageError( 'One or both of "mo_url" and "amqp_queue" must be specified') channel = Channel(self.redis, self.config, body, self.plugins) yield channel.start(self.service) yield channel.save() returnValue( response(request, 'channel created', (yield channel.status()))) @app.route('/channels/<string:channel_id>', methods=['GET']) @inlineCallbacks def get_channel(self, request, channel_id): '''Return the channel configuration and a nested status object''' channel = yield Channel.from_id(self.redis, self.config, channel_id, self.service, self.plugins) resp = yield channel.status() returnValue(response(request, 'channel found', resp)) @app.route('/channels/<string:channel_id>', methods=['POST']) @json_body @validate( body_schema({ 'type': 'object', 'properties': { 'type': { 'type': 'string' }, 'label': { 'type': 'string' }, 'config': { 'type': 'object' }, 'metadata': { 'type': 'object' }, 'status_url': { 'type': ['string', 'null'] }, 'mo_url': { 'type': ['string', 'null'] }, 'rate_limit_count': { 'type': 'integer', 'minimum': 0, }, 'rate_limit_window': { 'type': 'integer', 'minimum': 0, }, 'character_limit': { 'type': 'integer', 'minimum': 0, }, }, })) @inlineCallbacks def modify_channel(self, request, body, channel_id): '''Mondify the channel configuration''' channel = yield Channel.from_id(self.redis, self.config, channel_id, self.service, self.plugins) resp = yield channel.update(body) returnValue(response(request, 'channel updated', resp)) @app.route('/channels/<string:channel_id>', methods=['DELETE']) @inlineCallbacks def delete_channel(self, request, channel_id): '''Delete the channel''' channel = yield Channel.from_id(self.redis, self.config, channel_id, self.service, self.plugins) yield channel.stop() yield channel.delete() returnValue(response(request, 'channel deleted', {})) @app.route('/channels/<string:channel_id>/restart', methods=['POST']) @inlineCallbacks def restart_channel(self, request, channel_id): '''Restart a channel.''' channel = yield Channel.from_id(self.redis, self.config, channel_id, self.service, self.plugins) yield channel.stop() yield channel.start(self.service) returnValue(response(request, 'channel restarted', {})) @app.route('/channels/<string:channel_id>/logs', methods=['GET']) @inlineCallbacks def get_logs(self, request, channel_id): '''Get the last N logs for a channel, sorted reverse chronologically.''' n = request.args.get('n', None) if n is not None: n = int(n[0]) channel = yield Channel.from_id(self.redis, self.config, channel_id, self.service, self.plugins) logs = yield channel.get_logs(n) returnValue(response(request, 'logs retrieved', logs)) @app.route('/channels/<string:channel_id>/messages/', methods=['POST']) @json_body @validate( body_schema({ 'type': 'object', 'properties': { 'to': { 'type': 'string' }, 'from': { 'type': ['string', 'null'] }, 'reply_to': { 'type': 'string' }, 'content': { 'type': ['string', 'null'] }, 'event_url': { 'type': 'string' }, 'priority': { 'type': 'string' }, 'channel_data': { 'type': 'object' }, }, 'required': ['content'], 'additionalProperties': False, })) @inlineCallbacks def send_message(self, request, body, channel_id): '''Send an outbound (mobile terminated) message''' if 'to' not in body and 'reply_to' not in body: raise ApiUsageError('Either "to" or "reply_to" must be specified') if 'to' in body and 'reply_to' in body: raise ApiUsageError( 'Only one of "to" and "reply_to" may be specified') if 'from' in body and 'reply_to' in body: raise ApiUsageError( 'Only one of "from" and "reply_to" may be specified') channel = yield Channel.from_id(self.redis, self.config, channel_id, self.service, self.plugins) if 'to' in body: msg = yield channel.send_message(self.message_sender, self.outbounds, body) else: msg = yield channel.send_reply_message(self.message_sender, self.outbounds, self.inbounds, body) yield self.message_rate.increment(channel_id, 'outbound', self.config.metric_window) returnValue(response(request, 'message sent', msg)) @app.route('/channels/<string:channel_id>/messages/<string:message_id>', methods=['GET']) @inlineCallbacks def get_message_status(self, request, channel_id, message_id): '''Retrieve the status of a message''' events = yield self.outbounds.load_all_events(channel_id, message_id) events = sorted((api_from_event(channel_id, e) for e in events), key=lambda e: e['timestamp']) last_event = events[-1] if events else None last_event_type = last_event['event_type'] if last_event else None last_event_timestamp = last_event['timestamp'] if last_event else None returnValue( response( request, 'message status', { 'id': message_id, 'last_event_type': last_event_type, 'last_event_timestamp': last_event_timestamp, 'events': events, })) @app.route('/health', methods=['GET']) def health_status(self, request): return response(request, 'health ok', {})
class JunebugApi(object): app = Klein() def __init__(self, service, config): self.service = service self.redis_config = config.redis self.amqp_config = config.amqp self.config = config @inlineCallbacks def setup(self, redis=None, message_sender=None): if redis is None: redis = yield TxRedisManager.from_config(self.redis_config) if message_sender is None: message_sender = MessageSender( 'amqp-spec-0-8.xml', self.amqp_config) self.redis = redis self.message_sender = message_sender self.message_sender.setServiceParent(self.service) self.inbounds = InboundMessageStore( self.redis, self.config.inbound_message_ttl) self.outbounds = OutboundMessageStore( self.redis, self.config.outbound_message_ttl) self.message_rate = MessageRateStore(self.redis) self.router_store = RouterStore(self.redis) self.plugins = [] for plugin_config in self.config.plugins: cls = load_class_by_string(plugin_config['type']) plugin = cls() yield plugin.start_plugin(plugin_config, self.config) self.plugins.append(plugin) yield Channel.start_all_channels( self.redis, self.config, self.service, self.plugins) yield Router.start_all_routers(self) if self.config.rabbitmq_management_interface: self.rabbitmq_management_client = RabbitmqManagementClient( self.config.rabbitmq_management_interface, self.amqp_config['username'], self.amqp_config['password']) @inlineCallbacks def teardown(self): yield self.redis.close_manager() for plugin in self.plugins: yield plugin.stop_plugin() @app.handle_errors(JunebugError) def generic_junebug_error(self, request, failure): return response(request, failure.value.description, { 'errors': [{ 'type': failure.value.name, 'message': failure.getErrorMessage(), }] }, code=failure.value.code) @app.handle_errors(HTTPException) def http_error(self, request, failure): error = { 'code': failure.value.code, 'type': failure.value.name, 'message': failure.getErrorMessage(), } if getattr(failure.value, 'new_url', None) is not None: request.setHeader('Location', failure.value.new_url) error['new_url'] = failure.value.new_url return response(request, failure.value.description, { 'errors': [error], }, code=failure.value.code) @app.handle_errors def generic_error(self, request, failure): log.err(failure) return response(request, 'generic error', { 'errors': [{ 'type': failure.type.__name__, 'message': failure.getErrorMessage(), }] }, code=http.INTERNAL_SERVER_ERROR) @app.route('/channels/', methods=['GET']) @inlineCallbacks def get_channel_list(self, request): '''List all channels''' ids = yield Channel.get_all(self.redis) returnValue(response(request, 'channels listed', sorted(ids))) @app.route('/channels/', methods=['POST']) @json_body @validate( body_schema({ 'type': 'object', 'properties': { 'type': {'type': 'string'}, 'label': {'type': 'string'}, 'config': {'type': 'object'}, 'metadata': {'type': 'object'}, 'status_url': {'type': 'string'}, 'mo_url': {'type': 'string'}, 'mo_url_auth_token': {'type': 'string'}, 'amqp_queue': {'type': 'string'}, 'rate_limit_count': { 'type': 'integer', 'minimum': 0, }, 'rate_limit_window': { 'type': 'integer', 'minimum': 0, }, 'character_limit': { 'type': 'integer', 'minimum': 0, }, }, 'required': ['type', 'config'], })) @inlineCallbacks def create_channel(self, request, body): '''Create a channel''' channel = Channel( self.redis, self.config, body, self.plugins) yield channel.start(self.service) yield channel.save() returnValue(response( request, 'channel created', (yield channel.status()), code=http.CREATED)) @app.route('/channels/<string:channel_id>', methods=['GET']) @inlineCallbacks def get_channel(self, request, channel_id): '''Return the channel configuration and a nested status object''' channel = yield Channel.from_id( self.redis, self.config, channel_id, self.service, self.plugins) resp = yield channel.status() returnValue(response( request, 'channel found', resp)) @app.route('/channels/<string:channel_id>', methods=['POST']) @json_body @validate( body_schema({ 'type': 'object', 'properties': { 'type': {'type': 'string'}, 'label': {'type': 'string'}, 'config': {'type': 'object'}, 'metadata': {'type': 'object'}, 'status_url': {'type': ['string', 'null']}, 'mo_url': {'type': ['string', 'null']}, 'rate_limit_count': { 'type': 'integer', 'minimum': 0, }, 'rate_limit_window': { 'type': 'integer', 'minimum': 0, }, 'character_limit': { 'type': 'integer', 'minimum': 0, }, }, })) @inlineCallbacks def modify_channel(self, request, body, channel_id): '''Mondify the channel configuration''' channel = yield Channel.from_id( self.redis, self.config, channel_id, self.service, self.plugins) resp = yield channel.update(body) returnValue(response( request, 'channel updated', resp)) @app.route('/channels/<string:channel_id>', methods=['DELETE']) @inlineCallbacks def delete_channel(self, request, channel_id): '''Delete the channel''' channel = yield Channel.from_id( self.redis, self.config, channel_id, self.service, self.plugins) yield channel.stop() yield channel.delete() returnValue(response( request, 'channel deleted', {})) @app.route('/channels/<string:channel_id>/restart', methods=['POST']) @inlineCallbacks def restart_channel(self, request, channel_id): '''Restart a channel.''' channel = yield Channel.from_id( self.redis, self.config, channel_id, self.service, self.plugins) yield channel.stop() yield channel.start(self.service) returnValue(response(request, 'channel restarted', {})) @app.route('/channels/<string:channel_id>/logs', methods=['GET']) @inlineCallbacks def get_logs(self, request, channel_id): '''Get the last N logs for a channel, sorted reverse chronologically.''' n = request.args.get('n', None) if n is not None: n = int(n[0]) channel = yield Channel.from_id( self.redis, self.config, channel_id, self.service, self.plugins) logs = yield channel.get_logs(n) returnValue(response(request, 'logs retrieved', logs)) @app.route('/channels/<string:channel_id>/messages/', methods=['POST']) @json_body @validate( body_schema({ 'type': 'object', 'properties': { 'to': {'type': 'string'}, 'from': {'type': ['string', 'null']}, 'group': {'type': ['string', 'null']}, 'reply_to': {'type': 'string'}, 'content': {'type': ['string', 'null']}, 'event_url': {'type': 'string'}, 'event_auth_token': {'type': 'string'}, 'priority': {'type': 'string'}, 'channel_data': {'type': 'object'}, }, 'required': ['content'], 'additionalProperties': False, })) @inlineCallbacks def send_message(self, request, body, channel_id): '''Send an outbound (mobile terminated) message''' if 'to' not in body and 'reply_to' not in body: raise ApiUsageError( 'Either "to" or "reply_to" must be specified') channel = yield Channel.from_id( self.redis, self.config, channel_id, self.service, self.plugins) if 'reply_to' in body: msg = yield channel.send_reply_message( self.message_sender, self.outbounds, self.inbounds, body, allow_expired_replies=self.config.allow_expired_replies) else: msg = yield channel.send_message( self.message_sender, self.outbounds, body) yield self.message_rate.increment( channel_id, 'outbound', self.config.metric_window) returnValue(response( request, 'message submitted', msg, code=http.CREATED)) @app.route( '/channels/<string:channel_id>/messages/<string:message_id>', methods=['GET']) @inlineCallbacks def get_message_status(self, request, channel_id, message_id): '''Retrieve the status of a message''' events = yield self.outbounds.load_all_events(channel_id, message_id) events = sorted( (api_from_event(channel_id, e) for e in events), key=lambda e: e['timestamp']) last_event = events[-1] if events else None last_event_type = last_event['event_type'] if last_event else None last_event_timestamp = last_event['timestamp'] if last_event else None returnValue(response(request, 'message status', { 'id': message_id, 'last_event_type': last_event_type, 'last_event_timestamp': last_event_timestamp, 'events': events, })) @app.route('/routers/', methods=['GET']) def get_router_list(self, request): """List all routers""" d = Router.get_all(self.router_store) d.addCallback(partial(response, request, 'routers retrieved')) return d @app.route('/routers/', methods=['POST']) @json_body @validate( body_schema({ 'type': 'object', 'properties': { 'type': {'type': 'string'}, 'label': {'type': 'string'}, 'config': {'type': 'object'}, 'metadata': {'type': 'object'}, }, 'additionalProperties': False, 'required': ['type', 'config'], })) @inlineCallbacks def create_router(self, request, body): """Create a new router""" router = Router(self, body) yield router.validate_config() router.start(self.service) yield router.save() returnValue(response( request, 'router created', (yield router.status()), code=http.CREATED )) @app.route('/routers/<string:router_id>', methods=['GET']) def get_router(self, request, router_id): """Get the configuration details and status of a specific router""" d = Router.from_id(self, router_id) d.addCallback(lambda router: router.status()) d.addCallback(partial(response, request, 'router found')) return d @app.route('/routers/<string:router_id>', methods=['PUT']) @json_body @validate( body_schema({ 'type': 'object', 'properties': { 'type': {'type': 'string'}, 'label': {'type': 'string'}, 'config': {'type': 'object'}, 'metadata': {'type': 'object'}, }, 'additionalProperties': False, 'required': ['type', 'config'], })) @inlineCallbacks def replace_router_config(self, request, body, router_id): """Replace the router config with the one specified""" router = yield Router.from_id(self, router_id) for field in ['type', 'label', 'config', 'metadata']: router.router_config.pop(field, None) router.router_config.update(body) yield router.validate_config() # Stop and start the router for the worker to get the new config yield router.stop() router.start(self.service) yield router.save() returnValue(response( request, 'router updated', (yield router.status()))) @app.route('/routers/<string:router_id>', methods=['PATCH']) @json_body @validate( body_schema({ 'type': 'object', 'properties': { 'type': {'type': 'string'}, 'label': {'type': 'string'}, 'config': {'type': 'object'}, 'metadata': {'type': 'object'}, }, 'additionalProperties': False, 'required': [], })) @inlineCallbacks def update_router_config(self, request, body, router_id): """Update the router config with the one specified""" router = yield Router.from_id(self, router_id) router.router_config.update(body) yield router.validate_config() # Stop and start the router for the worker to get the new config yield router.stop() router.start(self.service) yield router.save() returnValue(response( request, 'router updated', (yield router.status()))) @app.route('/routers/<string:router_id>', methods=['DELETE']) @inlineCallbacks def delete_router(self, request, router_id): router = yield Router.from_id(self, router_id) yield router.stop() yield router.delete() returnValue(response(request, 'router deleted', {})) @app.route('/routers/<string:router_id>/destinations/', methods=['POST']) @json_body @validate( body_schema({ 'type': 'object', 'properties': { 'label': {'type': 'string'}, 'config': {'type': 'object'}, 'metadata': {'type': 'object'}, 'mo_url': {'type': 'string'}, 'mo_url_token': {'type': 'string'}, 'amqp_queue': {'type': 'string'}, 'character_limit': { 'type': 'integer', 'minimum': 0, }, }, 'additionalProperties': False, 'required': ['config'], })) @inlineCallbacks def create_router_destination(self, request, body, router_id): """Create a new destination for the router""" router = yield Router.from_id(self, router_id) yield router.validate_destination_config(body['config']) destination = router.add_destination(body) yield router.stop() router.start(self.service) yield destination.save() returnValue(response( request, 'destination created', (yield destination.status()), code=http.CREATED )) @app.route('/routers/<string:router_id>/destinations/', methods=['GET']) def get_router_destination_list(self, request, router_id): """Get the list of destinations for a router""" d = Router.from_id(self, router_id) d.addCallback(lambda router: router.get_destination_list()) d.addCallback(partial(response, request, 'destinations retrieved')) return d @app.route( '/routers/<string:router_id>/destinations/<string:destination_id>', methods=['GET']) @inlineCallbacks def get_destination(self, request, router_id, destination_id): """Get the config and status of a destination""" router = yield Router.from_id(self, router_id) destination = router.get_destination(destination_id) returnValue(response( request, 'destination found', (yield destination.status()) )) @app.route( '/routers/<string:router_id>/destinations/<string:destination_id>', methods=['PUT']) @json_body @validate( body_schema({ 'type': 'object', 'properties': { 'label': {'type': 'string'}, 'config': {'type': 'object'}, 'metadata': {'type': 'object'}, 'mo_url': {'type': 'string'}, 'mo_url_token': {'type': 'string'}, 'amqp_queue': {'type': 'string'}, 'character_limit': { 'type': 'integer', 'minimum': 0, }, }, 'additionalProperties': False, 'required': ['config'], })) @inlineCallbacks def replace_router_destination( self, request, body, router_id, destination_id): """Replace the config of a router destination""" router = yield Router.from_id(self, router_id) yield router.validate_destination_config(body['config']) destination = router.get_destination(destination_id) destination.destination_config = body destination.destination_config['id'] = destination_id # Stop and start the router for the worker to get the new config yield router.stop() router.start(self.service) yield destination.save() returnValue(response( request, 'destination updated', (yield destination.status()))) @app.route( '/routers/<string:router_id>/destinations/<string:destination_id>', methods=['PATCH']) @json_body @validate( body_schema({ 'type': 'object', 'properties': { 'label': {'type': 'string'}, 'config': {'type': 'object'}, 'metadata': {'type': 'object'}, 'mo_url': {'type': 'string'}, 'mo_url_token': {'type': 'string'}, 'amqp_queue': {'type': 'string'}, 'character_limit': { 'type': 'integer', 'minimum': 0, }, }, 'additionalProperties': False, 'required': [], })) @inlineCallbacks def update_router_destination( self, request, body, router_id, destination_id): """Update the config of a router destination""" router = yield Router.from_id(self, router_id) if 'config' in body: yield router.validate_destination_config(body['config']) destination = router.get_destination(destination_id) destination.destination_config.update(body) # Stop and start the router for the worker to get the new config yield router.stop() router.start(self.service) yield destination.save() returnValue(response( request, 'destination updated', (yield destination.status()))) @app.route( '/routers/<string:router_id>/destinations/<string:destination_id>', methods=['DELETE']) @inlineCallbacks def delete_router_destination(self, request, router_id, destination_id): """Delete and stop the router destination""" router = yield Router.from_id(self, router_id) destination = router.get_destination(destination_id) yield router.stop() yield destination.delete() router.start(self.service) returnValue(response(request, 'destination deleted', {})) @app.route('/health', methods=['GET']) def health_status(self, request): if self.config.rabbitmq_management_interface: def get_queues(channel_ids): gets = [] for channel_id in channel_ids: for sub in ['inbound', 'outbound', 'event']: queue_name = "%s.%s" % (channel_id, sub) get = self.rabbitmq_management_client.get_queue( self.amqp_config['vhost'], queue_name) gets.append(get) return gets def return_queue_results(results): queues = [] stuck = False for result in results: queue = result[1] if ('messages' in queue): details = { 'name': queue['name'], 'stuck': False, 'messages': queue.get('messages'), 'rate': queue['messages_details']['rate'] } if (details['messages'] > 0 and details['rate'] == 0): stuck = True details['stuck'] = True queues.append(details) status = 'queues ok' code = http.OK if stuck: status = "queues stuck" code = http.INTERNAL_SERVER_ERROR return response(request, status, queues, code=code) d = Channel.get_all(self.redis) d.addCallback(get_queues) d.addCallback(defer.DeferredList) d.addCallback(return_queue_results) return d else: return response(request, 'health ok', {})
class JunebugApi(object): app = Klein() def __init__(self, service, config): self.service = service self.redis_config = config.redis self.amqp_config = config.amqp self.config = config @inlineCallbacks def setup(self, redis=None, message_sender=None): if redis is None: redis = yield TxRedisManager.from_config(self.redis_config) if message_sender is None: message_sender = MessageSender( 'amqp-spec-0-8.xml', self.amqp_config) self.redis = redis self.message_sender = message_sender self.message_sender.setServiceParent(self.service) self.inbounds = InboundMessageStore( self.redis, self.config.inbound_message_ttl) self.outbounds = OutboundMessageStore( self.redis, self.config.outbound_message_ttl) self.message_rate = MessageRateStore(self.redis) self.plugins = [] for plugin_config in self.config.plugins: cls = load_class_by_string(plugin_config['type']) plugin = cls() yield plugin.start_plugin(plugin_config, self.config) self.plugins.append(plugin) yield Channel.start_all_channels( self.redis, self.config, self.service, self.plugins) @inlineCallbacks def teardown(self): yield self.redis.close_manager() for plugin in self.plugins: yield plugin.stop_plugin() @app.handle_errors(JunebugError) def generic_junebug_error(self, request, failure): return response(request, failure.value.description, { 'errors': [{ 'type': failure.value.name, 'message': failure.getErrorMessage(), }] }, code=failure.value.code) @app.handle_errors(HTTPException) def http_error(self, request, failure): return response(request, failure.value.description, { 'errors': [{ 'type': failure.value.name, 'message': failure.getErrorMessage(), }] }, code=failure.value.code) @app.handle_errors def generic_error(self, request, failure): logging.exception(failure) return response(request, 'generic error', { 'errors': [{ 'type': failure.type.__name__, 'message': failure.getErrorMessage(), }] }, code=http.INTERNAL_SERVER_ERROR) @app.route('/channels/', methods=['GET']) @inlineCallbacks def get_channel_list(self, request): '''List all channels''' ids = yield Channel.get_all(self.redis) returnValue(response(request, 'channels listed', sorted(ids))) @app.route('/channels/', methods=['POST']) @json_body @validate( body_schema({ 'type': 'object', 'properties': { 'type': {'type': 'string'}, 'label': {'type': 'string'}, 'config': {'type': 'object'}, 'metadata': {'type': 'object'}, 'status_url': {'type': 'string'}, 'mo_url': {'type': 'string'}, 'amqp_queue': {'type': 'string'}, 'rate_limit_count': { 'type': 'integer', 'minimum': 0, }, 'rate_limit_window': { 'type': 'integer', 'minimum': 0, }, 'character_limit': { 'type': 'integer', 'minimum': 0, }, }, 'required': ['type', 'config'], })) @inlineCallbacks def create_channel(self, request, body): '''Create a channel''' if not (body.get('mo_url') or body.get('amqp_queue')): raise ApiUsageError( 'One or both of "mo_url" and "amqp_queue" must be specified') channel = Channel( self.redis, self.config, body, self.plugins) yield channel.save() yield channel.start(self.service) returnValue(response( request, 'channel created', (yield channel.status()))) @app.route('/channels/<string:channel_id>', methods=['GET']) @inlineCallbacks def get_channel(self, request, channel_id): '''Return the channel configuration and a nested status object''' channel = yield Channel.from_id( self.redis, self.config, channel_id, self.service, self.plugins) resp = yield channel.status() returnValue(response( request, 'channel found', resp)) @app.route('/channels/<string:channel_id>', methods=['POST']) @json_body @validate( body_schema({ 'type': 'object', 'properties': { 'type': {'type': 'string'}, 'label': {'type': 'string'}, 'config': {'type': 'object'}, 'metadata': {'type': 'object'}, 'status_url': {'type': 'string'}, 'mo_url': {'type': 'string'}, 'rate_limit_count': { 'type': 'integer', 'minimum': 0, }, 'rate_limit_window': { 'type': 'integer', 'minimum': 0, }, 'character_limit': { 'type': 'integer', 'minimum': 0, }, }, })) @inlineCallbacks def modify_channel(self, request, body, channel_id): '''Mondify the channel configuration''' channel = yield Channel.from_id( self.redis, self.config, channel_id, self.service, self.plugins) resp = yield channel.update(body) returnValue(response( request, 'channel updated', resp)) @app.route('/channels/<string:channel_id>', methods=['DELETE']) @inlineCallbacks def delete_channel(self, request, channel_id): '''Delete the channel''' channel = yield Channel.from_id( self.redis, self.config, channel_id, self.service, self.plugins) yield channel.stop() yield channel.delete() returnValue(response( request, 'channel deleted', {})) @app.route('/channels/<string:channel_id>/logs', methods=['GET']) @inlineCallbacks def get_logs(self, request, channel_id): '''Get the last N logs for a channel, sorted reverse chronologically.''' n = request.args.get('n', None) if n is not None: n = int(n[0]) channel = yield Channel.from_id( self.redis, self.config, channel_id, self.service, self.plugins) logs = yield channel.get_logs(n) returnValue(response(request, 'logs retrieved', logs)) @app.route('/channels/<string:channel_id>/messages/', methods=['POST']) @json_body @validate( body_schema({ 'type': 'object', 'properties': { 'to': {'type': 'string'}, 'from': {'type': ['string', 'null']}, 'reply_to': {'type': 'string'}, 'content': {'type': ['string', 'null']}, 'event_url': {'type': 'string'}, 'priority': {'type': 'string'}, 'channel_data': {'type': 'object'}, }, 'required': ['content'], 'additionalProperties': False, })) @inlineCallbacks def send_message(self, request, body, channel_id): '''Send an outbound (mobile terminated) message''' if 'to' not in body and 'reply_to' not in body: raise ApiUsageError( 'Either "to" or "reply_to" must be specified') if 'to' in body and 'reply_to' in body: raise ApiUsageError( 'Only one of "to" and "reply_to" may be specified') if 'from' in body and 'reply_to' in body: raise ApiUsageError( 'Only one of "from" and "reply_to" may be specified') channel = yield Channel.from_id( self.redis, self.config, channel_id, self.service, self.plugins) if 'to' in body: msg = yield channel.send_message( self.message_sender, self.outbounds, body) else: msg = yield channel.send_reply_message( self.message_sender, self.outbounds, self.inbounds, body) yield self.message_rate.increment( channel_id, 'outbound', self.config.metric_window) returnValue(response(request, 'message sent', msg)) @app.route( '/channels/<string:channel_id>/messages/<string:message_id>', methods=['GET']) @inlineCallbacks def get_message_status(self, request, channel_id, message_id): '''Retrieve the status of a message''' events = yield self.outbounds.load_all_events(channel_id, message_id) events = sorted( (api_from_event(channel_id, e) for e in events), key=lambda e: e['timestamp']) last_event = events[-1] if events else None last_event_type = last_event['event_type'] if last_event else None last_event_timestamp = last_event['timestamp'] if last_event else None returnValue(response(request, 'message status', { 'id': message_id, 'last_event_type': last_event_type, 'last_event_timestamp': last_event_timestamp, 'events': events, })) @app.route('/health', methods=['GET']) def health_status(self, request): return response(request, 'health ok', {})