boyska 2 years ago
commit
94a1962043
1 changed files with 227 additions and 0 deletions
  1. 227 0
      transfer_back.py

+ 227 - 0
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()