commit 94a19620435c81641fe38bfc78d17ba12a04c31b Author: boyska Date: Sat Feb 19 01:38:17 2022 +0100 initial diff --git a/transfer_back.py b/transfer_back.py new file mode 100755 index 0000000..76a2a65 --- /dev/null +++ b/transfer_back.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 + + +import asyncio +import logging + +from panoramisk import Manager +from panoramisk.actions import Action + +LOOP=asyncio.get_event_loop() + +log = logging.getLogger('transferback') + +async def on_tutto(manager, msg): + for prefix in ['Var', 'RTC']: + if msg.event.startswith(prefix): + return + log.debug('... Event %s', msg.event) + +class BaseApp: + def __init__(self, manager, msg): + self.manager = manager + self.initial_msg = msg + self.local_channel = msg.channel + self.events = [] + self.log = logging.getLogger(self.__class__.__name__) + + def send_agi_command(self, *args, **kwargs): + return self.manager.send_agi_command(self.local_channel, *args, **kwargs) + + def register_event(self, event, callback): + self.events.append( + (event, self.manager.register_event(event, callback))) + + def unregister_event(self, event_name, callback=None): + if callback is None: + for ev, callback in self.events: + if ev == event_name: + self.unregister_event(event_name, callback) + return + else: + self.events.remove((event_name, callback)) + self.manager.callbacks[event_name].remove(callback) + + def self_destroy(self): + for ev, callback in self.events: + self.manager.callbacks[ev].remove(callback) + +def filter_channel(channel): + def create_func(f): + def new_func(manager, event): + if event.channel != channel: + return + return f(manager, event) + return new_func + return create_func + + +class RefusedError(Exception): + pass + +class TransferAndTakeBack(BaseApp): + ''' + The idea is to provide a method to implement a transfer, and then be able to "revoke" the transfer whenever we want. + + In our scenario, there is a local user and a remote user. + The remote user must have no power. + The local user triggers it all. + + When the local user activates a feature, which calls AGI(agi:sync), this is called. + + This is the behavior we want: + - the remote user is trasferred to a well-known place + - the local user gets music + - when the local user presses "#", we + + ''' + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.is_context_auth(self.initial_msg.context): + raise RefusedError("Invalid context") + self.log.info('%s init: %s', self.__class__.__name__, self.initial_msg) + + + # meant to be overridden by children {{{ + def get_destination(self): + return 'private,9438,1' + + def get_backroom(self): + return 'private,9401,1' + + def get_waiting_room(self): + return 'private,9401,1' + + def is_context_auth(self, context): + return context == 'from-regia' + + # }}} + + async def run(self): + self.channels = [] + self.register_event('CoreShowChannel', self.on_channel) + self.register_event('CoreShowChannelsComplete', self.on_channels) + self.manager.send_action(Action({'Action': 'CoreShowChannels'}), as_list=False) + # the response will actually be a series of CoreShowChannel, which will trigger self.on_channel + + def on_channel(self, _, msg): + self.channels.append(msg) + + async def on_channels(self, _, msg): + self.unregister_event('CoreShowChannel') + self.unregister_event('CoreShowChannelsComplete') + # we've got everything in many "on_channel" invocation, now let's check better + our_channel = [m for m in self.channels if m.channel == self.local_channel] + assert len(our_channel) == 1 + our_channel = our_channel[0] + bridgeid = our_channel.bridgeid + + channels_in_bridge = [m for m in self.channels if m.bridgeid == bridgeid and m.channel != self.local_channel] + self.remote_channel = channels_in_bridge[0].channel.split(';')[0] + + self.log.info('FOUND: %s', self.remote_channel) + await self.on_remote_channel_found() + + async def on_remote_channel_found(self): + # now we know what is the other channel; this means we can move them wherever we want + cmd = 'channel redirect {other} {dest}'.format( + other=self.remote_channel, + dest=self.get_destination() + ) + self.log.debug('CMD = %s', cmd) + self.manager.send_command(cmd) + + # let's wait them somewhere + cmd = 'channel redirect {channel} {waitingroom}'.format( + channel=self.local_channel, + waitingroom=self.get_waiting_room(), + ) + self.log.debug('CMD = %s', cmd) + self.manager.send_command(cmd) + + # eventi possibili: + # - l'utente locale preme dei tasti + # - l'utente locale riaggancia + self.register_event('DTMFEnd', filter_channel(self.local_channel)(self.on_dtmf)) + self.register_event('Hangup', filter_channel(self.local_channel)(self.on_hangup)) + self.register_event('Hangup', filter_channel(self.remote_channel)(self.on_hangup)) + + + def on_dtmf(self, _, msg): + ''' + when the local user uses dtmf, they can talk back to the outside user + ''' + if msg.digit != '9': + return + self.unregister_event('DTMFEnd') + # a questo punto possiamo procedere a spostare il canale iniziale + self.log.info("Local user requested to talk back with the remote user") + + to_move = [self.remote_channel] + if self.get_backroom() != self.get_waiting_room(): + to_move.append(self.local_channel) + for channel in to_move: + + cmd = 'channel redirect {chan} {newroom}'.format( + chan=channel, + newroom=self.get_backroom(), + ) + self.log.debug('CMD = %s', cmd) + self.manager.send_command(cmd) + + # we must not self_destroy, or we'll never notice the Hangup + assert set(e[0] for e in self.events) == {'Hangup'} + + def on_hangup(self, _, msg): + if msg.channel == self.remote_channel: + self.log.info('Remote user hanged up') + channel = self.local_channel + elif msg.channel == self.local_channel: + self.log.info('Local user hanged up') + channel = self.remote_channel + else: + self.log.error('This should never happen! someone else hanged??? %s', msg.channel) + return + cmd = 'channel request hangup {channel}'.format(channel=channel) + self.log.debug('CMD = %s', cmd) + self.manager.send_command(cmd) + + self.self_destroy() + +def run_app(Cls): + async def real_run(manager, msg): + try: + instance = Cls(manager, msg) + except RefusedError: + return + await instance.run() + return real_run + + +async def init(manager): + # manager.send_command('sip show peers') + + manager.register_event('*', on_tutto) + manager.register_event('AsyncAGIStart', run_app(TransferAndTakeBack)) + +async def on_shutdown(m): + pass + +def main(): + logging.basicConfig(level=logging.INFO) + manager = Manager( + loop=LOOP, + host='127.0.0.1', + port=5038, + ssl=False, + encoding='utf8', + username='admin', + secret='secret123password', + ping_delay=10, # Delay after start + ping_interval=10, # Periodically ping AMI (dead or alive) + reconnect_timeout=2, # Timeout reconnect if connection lost + ) + manager.connect(run_forever=True, on_startup=init, on_shutdown=on_shutdown) + +if __name__ == '__main__': + main()