transfer_back.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. #!/usr/bin/env python3
  2. import logging
  3. import urllib.parse
  4. from argparse import ArgumentParser
  5. from panoramisk.actions import Action
  6. import asterisk_misc_common
  7. log = logging.getLogger("transferback")
  8. async def on_tutto(manager, msg):
  9. for prefix in ["Var", "RTC"]:
  10. if msg.event.startswith(prefix):
  11. return
  12. log.debug("... Event %s", msg.event)
  13. class BaseApp:
  14. def __init__(self, manager, msg):
  15. self.manager = manager
  16. self.initial_msg = msg
  17. self.local_channel = msg.channel
  18. self.events = []
  19. self.log = logging.getLogger(self.__class__.__name__)
  20. def send_agi_command(self, *args, **kwargs):
  21. return self.manager.send_agi_command(self.local_channel, *args, **kwargs)
  22. def register_event(self, event, callback):
  23. self.events.append((event, self.manager.register_event(event, callback)))
  24. def unregister_event(self, event_name, callback=None):
  25. if callback is None:
  26. for ev, callback in self.events:
  27. if ev == event_name:
  28. self.unregister_event(event_name, callback)
  29. return
  30. else:
  31. self.events.remove((event_name, callback))
  32. self.manager.callbacks[event_name].remove(callback)
  33. def self_destroy(self):
  34. for ev, callback in self.events:
  35. self.manager.callbacks[ev].remove(callback)
  36. def parse_env(self, env: str) -> dict:
  37. lines = urllib.parse.unquote(env).split("\n")
  38. ret = {}
  39. for line in lines:
  40. if ": " in line:
  41. key, value = line.split(": ", 1)
  42. ret[key] = value
  43. return ret
  44. def filter_channel(channel):
  45. def create_func(f):
  46. def new_func(manager, event):
  47. if event.channel != channel:
  48. return
  49. return f(manager, event)
  50. return new_func
  51. return create_func
  52. class RefusedError(Exception):
  53. pass
  54. class TransferAndTakeBack(BaseApp):
  55. """
  56. The idea is to provide a method to implement a transfer, and then be able to "revoke" the transfer whenever we want.
  57. In our scenario, there is a local user and a remote user.
  58. The remote user must have no power.
  59. The local user triggers it all.
  60. When the local user activates a feature, which calls AGI(agi:sync), this is called.
  61. This is the behavior we want:
  62. - the remote user is trasferred to a well-known place
  63. - the local user gets music
  64. - when the local user presses "#", we
  65. """
  66. def __init__(self, *args, **kwargs):
  67. super().__init__(*args, **kwargs)
  68. context = self.initial_msg.context
  69. if not self.is_context_auth(context):
  70. raise RefusedError("Invalid context: %s" % context)
  71. env = self.parse_env(self.initial_msg.env)
  72. command_name = env.get("agi_arg_1", "")
  73. if not command_name.startswith("mandainonda"):
  74. raise RefusedError("Invalid command name: %s" % command_name)
  75. self.log.info("init: %s", self.initial_msg)
  76. self.log.info("init: %s", command_name)
  77. # meant to be overridden by children {{{
  78. def get_destination(self):
  79. """
  80. la destination e' tipicamente una confbridge in cui l'utente remoto
  81. incontra lo speaker
  82. """
  83. return "private,9438,1"
  84. def get_backroom(self):
  85. """
  86. nella backroom, l'utente locale si incontra nuovamente con l'utente
  87. remoto
  88. """
  89. return "private,9401,1"
  90. def get_waiting_room(self):
  91. """
  92. nella waiting room, l'utente locale sente musica d'attesa mentre aspetta
  93. che una di queste due cose avvenga:
  94. - l'utente remoto riaggancia
  95. - l'utente locale riaggancia
  96. - l'utente locale preme #
  97. """
  98. return "private,9401,1"
  99. def is_context_auth(self, context):
  100. return context == "from-regia"
  101. # }}}
  102. async def run(self):
  103. self.channels = []
  104. self.register_event("CoreShowChannel", self.on_channel)
  105. self.register_event("CoreShowChannelsComplete", self.on_channels)
  106. self.manager.send_action(Action({"Action": "CoreShowChannels"}), as_list=False)
  107. # the response will actually be a series of CoreShowChannel, which will
  108. # trigger self.on_channel, followed by a CoreShowChannelsComplete, which
  109. # will trigger on_channels
  110. def on_channel(self, _, msg):
  111. self.channels.append(msg)
  112. async def on_channels(self, _, msg):
  113. self.unregister_event("CoreShowChannel")
  114. self.unregister_event("CoreShowChannelsComplete")
  115. # we've got everything in many "on_channel" invocation, now let's check better
  116. our_bridge = {
  117. m.bridgeid for m in self.channels if m.channel == self.local_channel
  118. }
  119. self.log.debug("valid channels: %s", str(our_bridge))
  120. assert len(our_bridge) == 1
  121. bridgeid = next(iter(our_bridge))
  122. channels_in_bridge = [
  123. m
  124. for m in self.channels
  125. if m.bridgeid == bridgeid and m.channel != self.local_channel
  126. ]
  127. self.remote_channel = channels_in_bridge[0].channel.split(";")[0]
  128. self.log.info("FOUND: %s", self.remote_channel)
  129. await self.on_remote_channel_found()
  130. async def on_remote_channel_found(self):
  131. # now we know what is the other channel; this means we can move them wherever we want
  132. cmd = "channel redirect {other} {dest}".format(
  133. other=self.remote_channel, dest=self.get_destination()
  134. )
  135. self.log.debug("CMD = %s", cmd)
  136. self.manager.send_command(cmd)
  137. # let's wait them somewhere
  138. cmd = "channel redirect {channel} {waitingroom}".format(
  139. channel=self.local_channel,
  140. waitingroom=self.get_waiting_room(),
  141. )
  142. self.log.debug("CMD = %s", cmd)
  143. self.manager.send_command(cmd)
  144. # eventi possibili:
  145. # - l'utente locale preme dei tasti
  146. # - l'utente locale riaggancia
  147. self.register_event("DTMFEnd", filter_channel(self.local_channel)(self.on_dtmf))
  148. self.register_event(
  149. "Hangup", filter_channel(self.local_channel)(self.on_hangup)
  150. )
  151. self.register_event(
  152. "Hangup", filter_channel(self.remote_channel)(self.on_hangup)
  153. )
  154. def on_dtmf(self, _, msg):
  155. """
  156. when the local user uses dtmf, they can talk back to the outside user
  157. """
  158. if msg.digit != "9":
  159. return
  160. self.unregister_event("DTMFEnd")
  161. # a questo punto possiamo procedere a spostare il canale iniziale
  162. self.log.info("Local user requested to talk back with the remote user")
  163. to_move = [self.remote_channel]
  164. if self.get_backroom() != self.get_waiting_room():
  165. to_move.append(self.local_channel)
  166. for channel in to_move:
  167. cmd = "channel redirect {chan} {newroom}".format(
  168. chan=channel,
  169. newroom=self.get_backroom(),
  170. )
  171. self.log.debug("CMD = %s", cmd)
  172. self.manager.send_command(cmd)
  173. # we must not self_destroy, or we'll never notice the Hangup
  174. assert set(e[0] for e in self.events) == {"Hangup"}
  175. def on_hangup(self, _, msg):
  176. if msg.channel == self.remote_channel:
  177. self.log.info("Remote user hanged up")
  178. channel = self.local_channel
  179. elif msg.channel == self.local_channel:
  180. self.log.info("Local user hanged up")
  181. channel = self.remote_channel
  182. else:
  183. self.log.error(
  184. "This should never happen! someone else hanged??? %s", msg.channel
  185. )
  186. return
  187. cmd = "channel request hangup {channel}".format(channel=channel)
  188. self.log.debug("CMD = %s", cmd)
  189. self.manager.send_command(cmd)
  190. self.self_destroy()
  191. def run_app(Cls):
  192. async def real_run(manager, msg):
  193. try:
  194. instance = Cls(manager, msg)
  195. except RefusedError as exc:
  196. log.info("refused: %s", exc)
  197. return
  198. await instance.run()
  199. return real_run
  200. async def init(manager):
  201. # manager.send_command('sip show peers')
  202. manager.register_event("*", on_tutto)
  203. manager.register_event("AsyncAGIStart", run_app(TransferAndTakeBack))
  204. async def on_shutdown(m):
  205. pass
  206. def main():
  207. p = ArgumentParser()
  208. asterisk_misc_common.add_arguments(p)
  209. args = p.parse_args()
  210. asterisk_misc_common.do_common_options(args)
  211. manager = asterisk_misc_common.get_manager(args)
  212. asterisk_misc_common.run_manager(
  213. manager, args, on_startup=init, on_shutdown=on_shutdown
  214. )
  215. if __name__ == "__main__":
  216. main()