class TransfersStasis: def __init__(self, amid_client, ari_client, services, state_factory, state_persistor, xivo_uuid): self.ari = ari_client self.amid = amid_client self.services = services self.xivo_uuid = xivo_uuid self.stasis_start_pubsub = Pubsub() self.stasis_start_pubsub.set_exception_handler(self.invalid_event) self.hangup_pubsub = Pubsub() self.hangup_pubsub.set_exception_handler(self.invalid_event) self.state_factory = state_factory self.state_persistor = state_persistor def subscribe(self): self.ari.on_application_registered(DEFAULT_APPLICATION_NAME, self.process_lost_hangups) self.ari.on_channel_event('ChannelEnteredBridge', self.release_hangup_lock) self.ari.on_channel_event('ChannelDestroyed', self.bypass_hangup_lock_from_source) self.ari.on_bridge_event('BridgeDestroyed', self.clean_bridge_variables) self.ari.on_channel_event('ChannelLeftBridge', self.clean_bridge) self.ari.on_channel_event('StasisStart', self.stasis_start) self.stasis_start_pubsub.subscribe('transfer_recipient_called', self.transfer_recipient_answered) self.stasis_start_pubsub.subscribe('create_transfer', self.create_transfer) self.ari.on_channel_event('ChannelDestroyed', self.hangup) self.ari.on_channel_event('StasisEnd', self.hangup) self.ari.on_channel_event('ChannelMohStop', self.moh_stop) self.hangup_pubsub.subscribe(TransferRole.recipient, self.recipient_hangup) self.hangup_pubsub.subscribe(TransferRole.initiator, self.initiator_hangup) self.hangup_pubsub.subscribe(TransferRole.transferred, self.transferred_hangup) self.ari.on_channel_event('ChannelCallerId', self.update_transfer_caller_id) def moh_stop(self, channel, event): logger.debug('received ChannelMohStop for channel %s (%s)', channel.id, event['channel']['name']) try: transfer = self.state_persistor.get_by_channel(channel.id) except KeyError: logger.debug('ignoring ChannelMohStop event: channel %s, app %s', event['channel']['name'], event['application']) return transfer_role = transfer.role(channel.id) if transfer_role != TransferRole.transferred: logger.debug('ignoring ChannelMohStop event: channel %s, app %s', event['channel']['name'], event['application']) return transfer_state = self.state_factory.make(transfer) transfer_state.transferred_moh_stop() def invalid_event(self, _, __, exception): if isinstance(exception, InvalidEvent): event = exception.event logger.error('invalid stasis event received: %s', event) elif (isinstance(exception, XiVOAmidError) or isinstance(exception, TransferException)): self.handle_error(exception) else: raise exception def handle_error(self, exception): logger.error('%s: %s', exception.message, exception.details) def process_lost_hangups(self): transfers = list(self.state_persistor.list()) logger.debug('Processing lost hangups since last stop...') for transfer in transfers: transfer_state = self.state_factory.make(transfer) if not Channel(transfer.transferred_call, self.ari).exists(): logger.debug('Transferred hangup from transfer %s', transfer.id) transfer_state = transfer_state.transferred_hangup() if not Channel(transfer.initiator_call, self.ari).exists(): logger.debug('Initiator hangup from transfer %s', transfer.id) transfer_state = transfer_state.initiator_hangup() if not Channel(transfer.recipient_call, self.ari).exists(): logger.debug('Recipient hangup from transfer %s', transfer.id) transfer_state = transfer_state.recipient_hangup() logger.debug('Done.') def stasis_start(self, event_objects, event): channel = event_objects['channel'] try: app_action = event['args'][1] except IndexError: logger.debug( 'ignoring StasisStart event: channel %s, app %s, args %s', event['channel']['name'], event['application'], event['args']) return self.stasis_start_pubsub.publish(app_action, (channel, event)) def hangup(self, channel, event): try: transfer = self.state_persistor.get_by_channel(channel.id) except KeyError: logger.debug('ignoring StasisEnd event: channel %s, app %s', event['channel']['name'], event['application']) return transfer_role = transfer.role(channel.id) self.hangup_pubsub.publish(transfer_role, transfer) def transfer_recipient_answered(self, channel_event): channel, event = channel_event event = TransferRecipientAnsweredEvent(event) try: transfer_bridge = self.ari.bridges.get( bridgeId=event.transfer_bridge) transfer_bridge.addChannel(channel=channel.id) except ARINotFound: logger.error('recipient answered, but transfer was hung up') return try: transfer = self.state_persistor.get(event.transfer_bridge) except KeyError: logger.debug('recipient answered, but transfer was abandoned') for channel_id in transfer_bridge.json['channels']: try: ari_helpers.unring_initiator_call(self.ari, channel_id) except ARINotFound: pass else: logger.debug('recipient answered, transfer continues normally') transfer_state = self.state_factory.make(transfer) transfer_state.recipient_answer() def create_transfer(self, channel_event): channel, event = channel_event event = CreateTransferEvent(event) try: bridge = self.ari.bridges.get(bridgeId=event.transfer_id) except ARINotFound: bridge = self.ari.bridges.createWithId(type='mixing', name='transfer', bridgeId=event.transfer_id) bridge.addChannel(channel=channel.id) channel_ids = bridge.get().json['channels'] if len(channel_ids) == 2: transfer = self.state_persistor.get(event.transfer_id) try: context = self.ari.channels.getChannelVar( channelId=transfer.initiator_call, variable='XIVO_TRANSFER_RECIPIENT_CONTEXT')['value'] exten = self.ari.channels.getChannelVar( channelId=transfer.initiator_call, variable='XIVO_TRANSFER_RECIPIENT_EXTEN')['value'] variables_str = self.ari.channels.getChannelVar( channelId=transfer.initiator_call, variable='XIVO_TRANSFER_VARIABLES')['value'] timeout_str = self.ari.channels.getChannelVar( channelId=transfer.initiator_call, variable='XIVO_TRANSFER_TIMEOUT')['value'] except ARINotFound: logger.error('initiator hung up while creating transfer') try: variables = json.loads(variables_str) except ValueError: logger.warning('could not decode transfer variables "%s"', variables_str) variables = {} timeout = None if timeout_str == 'None' else int(timeout_str) transfer_state = self.state_factory.make(transfer) new_state = transfer_state.start(transfer, context, exten, variables, timeout) if new_state.transfer.flow == 'blind': new_state.complete() def recipient_hangup(self, transfer): logger.debug('recipient hangup = cancel transfer %s', transfer.id) transfer_state = self.state_factory.make(transfer) transfer_state.recipient_hangup() def initiator_hangup(self, transfer): logger.debug('initiator hangup = complete transfer %s', transfer.id) transfer_state = self.state_factory.make(transfer) transfer_state.initiator_hangup() def transferred_hangup(self, transfer): logger.debug('transferred hangup = abandon transfer %s', transfer.id) transfer_state = self.state_factory.make(transfer) transfer_state.transferred_hangup() def clean_bridge(self, channel, event): try: bridge = self.ari.bridges.get(bridgeId=event['bridge']['id']) except ARINotFound: return if bridge.json['bridge_type'] != 'mixing': return logger.debug('cleaning bridge %s', bridge.id) try: self.ari.channels.get(channelId=channel.id) channel_is_hungup = False except ARINotFound: logger.debug('channel who left was hungup') channel_is_hungup = True if len(bridge.json['channels']) == 1 and channel_is_hungup: logger.debug('one channel left in bridge %s', bridge.id) lone_channel_id = bridge.json['channels'][0] try: bridge_is_locked = HangupLock.from_target(self.ari, bridge.id) except InvalidLock: bridge_is_locked = False if not bridge_is_locked: logger.debug('emptying bridge %s', bridge.id) try: self.ari.channels.hangup(channelId=lone_channel_id) except ARINotFound: pass try: bridge = bridge.get() except ARINotFound: return if len(bridge.json['channels']) == 0: self.bypass_hangup_lock_from_target(bridge) logger.debug('destroying bridge %s', bridge.id) try: bridge.destroy() except (ARINotInStasis, ARINotFound): pass def clean_bridge_variables(self, bridge, event): global_variable = 'XIVO_BRIDGE_VARIABLES_{}'.format(bridge.id) self.ari.asterisk.setGlobalVar(variable=global_variable, value='') def release_hangup_lock(self, channel, event): lock_source = channel lock_target_candidate_id = event['bridge']['id'] try: lock = HangupLock(self.ari, lock_source.id, lock_target_candidate_id) lock.release() except InvalidLock: pass def bypass_hangup_lock_from_source(self, channel, event): lock_source = channel for lock in HangupLock.from_source(self.ari, lock_source.id): lock.kill_target() def bypass_hangup_lock_from_target(self, bridge): try: lock = HangupLock.from_target(self.ari, bridge.id) lock.kill_source() except InvalidLock: pass def update_transfer_caller_id(self, channel, event): try: transfer = self.state_persistor.get_by_channel(channel.id) except KeyError: logger.debug('ignoring ChannelCallerId event: channel %s', event['channel']['name']) return transfer_role = transfer.role(channel.id) if transfer_role != TransferRole.recipient: logger.debug('ignoring ChannelCallerId event: channel %s', event['channel']['name']) return try: ari_helpers.update_connectedline(self.ari, self.amid, transfer.initiator_call, transfer.recipient_call) except ARINotFound: try: ari_helpers.update_connectedline(self.ari, self.amid, transfer.transferred_call, transfer.recipient_call) except ARINotFound: logger.debug( 'cannot update transfer callerid: everyone hung up')
class TestPubsub(unittest.TestCase): def setUp(self): self.pubsub = Pubsub() def test_subscribe_and_publish(self): callback = Mock() self.pubsub.subscribe(SOME_TOPIC, callback) self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) callback.assert_called_once_with(SOME_MESSAGE) def test_multiple_subscribe_on_same_topic_and_one_publish(self): callback_1 = Mock() callback_2 = Mock() self.pubsub.subscribe(SOME_TOPIC, callback_1) self.pubsub.subscribe(SOME_TOPIC, callback_2) self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) callback_1.assert_called_once_with(SOME_MESSAGE) callback_2.assert_called_once_with(SOME_MESSAGE) def test_multiple_subscribe_on_different_topics_and_two_publish(self): callback = Mock() message_1 = Mock() message_2 = Mock() topic_1 = 'abcd' topic_2 = 'efgh' self.pubsub.subscribe(topic_1, callback) self.pubsub.subscribe(topic_2, callback) self.pubsub.publish(topic_1, message_1) self.pubsub.publish(topic_2, message_2) callback.assert_any_call(message_1) callback.assert_any_call(message_2) self.assertEquals(callback.call_count, 2) def test_unsubscribe_when_never_subscribed(self): callback = Mock() try: self.pubsub.unsubscribe(SOME_TOPIC, callback) except Exception: self.fail('unsubscribe should not raise exceptions') def test_unsubscribed_when_subscribed(self): callback = Mock() self.pubsub.subscribe(SOME_TOPIC, callback) self.pubsub.unsubscribe(SOME_TOPIC, callback) self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) self.assertEquals(callback.call_count, 0) def publish_when_nobody_subscribed(self): try: self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) except Exception: self.fail('publish should not raise exceptions') def test_unsubscribe_when_multiple_subscribers_on_same_topic(self): callback_1 = Mock() callback_2 = Mock() self.pubsub.subscribe(SOME_TOPIC, callback_1) self.pubsub.subscribe(SOME_TOPIC, callback_2) self.pubsub.unsubscribe(SOME_TOPIC, callback_1) self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) assert_that(not_(callback_1.called)) callback_2.assert_called_once_with(SOME_MESSAGE) def test_when_exception_then_exception_is_handled(self): callback = Mock() exception = callback.side_effect = Exception() handler = Mock() self.pubsub.set_exception_handler(handler) self.pubsub.subscribe(SOME_TOPIC, callback) self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) handler.assert_called_once_with(callback, SOME_MESSAGE, exception) @patch('xivo.pubsub.logger') def test_when_exception_then_exception_is_logged_by_default(self, logger): callback = Mock() exception = callback.side_effect = Exception() self.pubsub.subscribe(SOME_TOPIC, callback) self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) logger.exception.assert_called_once_with(exception) def test_when_exception_then_other_callbacks_are_run(self): callback_1, callback_2, callback_3 = Mock(), Mock(), Mock() callback_2.side_effect = Exception() self.pubsub.subscribe(SOME_TOPIC, callback_1) self.pubsub.subscribe(SOME_TOPIC, callback_2) self.pubsub.subscribe(SOME_TOPIC, callback_3) self.pubsub.publish(SOME_TOPIC, SOME_MESSAGE) assert_that(callback_1.called) assert_that(callback_3.called)
class TransfersStasis(object): def __init__(self, amid_client, ari_client, services, state_factory, state_persistor, xivo_uuid): self.ari = ari_client self.amid = amid_client self.services = services self.xivo_uuid = xivo_uuid self.stasis_start_pubsub = Pubsub() self.stasis_start_pubsub.set_exception_handler(self.invalid_event) self.hangup_pubsub = Pubsub() self.hangup_pubsub.set_exception_handler(self.invalid_event) self.state_factory = state_factory self.state_persistor = state_persistor def subscribe(self): self.ari.on_application_registered(APPLICATION_NAME, self.process_lost_hangups) self.ari.on_channel_event('ChannelEnteredBridge', self.release_hangup_lock) self.ari.on_channel_event('ChannelDestroyed', self.bypass_hangup_lock_from_source) self.ari.on_bridge_event('BridgeDestroyed', self.clean_bridge_variables) self.ari.on_channel_event('ChannelLeftBridge', self.clean_bridge) self.ari.on_channel_event('StasisStart', self.stasis_start) self.stasis_start_pubsub.subscribe('transfer_recipient_called', self.transfer_recipient_answered) self.stasis_start_pubsub.subscribe('create_transfer', self.create_transfer) self.ari.on_channel_event('ChannelDestroyed', self.hangup) self.ari.on_channel_event('StasisEnd', self.hangup) self.hangup_pubsub.subscribe(TransferRole.recipient, self.recipient_hangup) self.hangup_pubsub.subscribe(TransferRole.initiator, self.initiator_hangup) self.hangup_pubsub.subscribe(TransferRole.transferred, self.transferred_hangup) self.ari.on_channel_event('ChannelCallerId', self.update_transfer_caller_id) def invalid_event(self, _, __, exception): if isinstance(exception, InvalidEvent): event = exception.event logger.error('invalid stasis event received: %s', event) elif (isinstance(exception, XiVOAmidError) or isinstance(exception, TransferException)): self.handle_error(exception) else: raise def handle_error(self, exception): logger.error('%s: %s', exception.message, exception.details) def process_lost_hangups(self): transfers = list(self.state_persistor.list()) logger.debug('Processing lost hangups since last stop...') for transfer in transfers: transfer_state = self.state_factory.make(transfer) if not ari_helpers.channel_exists(self.ari, transfer.transferred_call): logger.debug('Transferred hangup from transfer %s', transfer.id) transfer_state = transfer_state.transferred_hangup() if not ari_helpers.channel_exists(self.ari, transfer.initiator_call): logger.debug('Initiator hangup from transfer %s', transfer.id) transfer_state = transfer_state.initiator_hangup() if not ari_helpers.channel_exists(self.ari, transfer.recipient_call): logger.debug('Recipient hangup from transfer %s', transfer.id) transfer_state = transfer_state.recipient_hangup() logger.debug('Done.') def stasis_start(self, event_objects, event): channel = event_objects['channel'] try: app_action = event['args'][1] except IndexError: logger.debug('ignoring StasisStart event: channel %s, app %s, args %s', event['channel']['name'], event['application'], event['args']) return self.stasis_start_pubsub.publish(app_action, (channel, event)) def hangup(self, channel, event): try: transfer = self.state_persistor.get_by_channel(channel.id) except KeyError: logger.debug('ignoring StasisEnd event: channel %s, app %s', event['channel']['name'], event['application']) return transfer_role = transfer.role(channel.id) self.hangup_pubsub.publish(transfer_role, transfer) def transfer_recipient_answered(self, (channel, event)): event = TransferRecipientAnsweredEvent(event) try: transfer_bridge = self.ari.bridges.get(bridgeId=event.transfer_bridge) transfer_bridge.addChannel(channel=channel.id) except ARINotFound: logger.error('recipient answered, but transfer was hung up') return try: transfer = self.state_persistor.get(event.transfer_bridge) except KeyError: logger.debug('recipient answered, but transfer was abandoned') for channel_id in transfer_bridge.json['channels']: try: ari_helpers.unring_initiator_call(self.ari, channel_id) except ARINotFound: pass else: logger.debug('recipient answered, transfer continues normally') transfer_state = self.state_factory.make(transfer) transfer_state.recipient_answer()