From d6c41da2cbf42851374ef60949446df978963971 Mon Sep 17 00:00:00 2001 From: Michael Tremer Date: Wed, 28 Jun 2023 13:35:16 +0000 Subject: [PATCH] users: Connect to Asterisk and show any ongoing calls Signed-off-by: Michael Tremer --- Makefile.am | 1 + src/backend/accounts.py | 9 +-- src/backend/asterisk.py | 114 ++++++++++++++++++++++++++++++++++ src/backend/base.py | 2 + src/backend/util.py | 4 ++ src/templates/users/show.html | 31 ++++++++- src/web/users.py | 7 ++- 7 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 src/backend/asterisk.py diff --git a/Makefile.am b/Makefile.am index fbf9d3bd..b1749141 100644 --- a/Makefile.am +++ b/Makefile.am @@ -49,6 +49,7 @@ CLEANFILES += \ backend_PYTHON = \ src/backend/__init__.py \ src/backend/accounts.py \ + src/backend/asterisk.py \ src/backend/base.py \ src/backend/blog.py \ src/backend/bugzilla.py \ diff --git a/src/backend/accounts.py b/src/backend/accounts.py index e43c76d5..3c248f2b 100644 --- a/src/backend/accounts.py +++ b/src/backend/accounts.py @@ -829,7 +829,6 @@ class Account(LDAPObject): return "postfixMailUser" in self.classes def has_sip(self): - return False # XXX return "sipUser" in self.classes or "sipRoutingObject" in self.classes def is_lwl(self): @@ -1148,9 +1147,11 @@ class Account(LDAPObject): return sip_registrations - @lazy_property - def sip_channels(self): - return self.backend.talk.freeswitch.get_sip_channels(self) + async def get_sip_channels(self): + if not self.has_sip(): + return [] + + return await self.backend.asterisk.get_sip_channels(self.sip_id) def get_cdr(self, date=None, limit=None): return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit) diff --git a/src/backend/asterisk.py b/src/backend/asterisk.py new file mode 100644 index 00000000..8a478bfb --- /dev/null +++ b/src/backend/asterisk.py @@ -0,0 +1,114 @@ +#!/usr/bin/python3 + +import asyncio +import datetime +import logging +import panoramisk + +from . import misc +from .decorators import * + +loop = asyncio.get_event_loop() + +class Asterisk(misc.Object): + def init(self): + self.__manager = None + + # Connect as soon as the event loop starts + loop.create_task(self.connect()) + + @property + def manager(self): + if not self.__manager: + raise RuntimeError("Asterisk is not connected") + + return self.__manager + + async def connect(self): + """ + Connects to Asterisk + """ + manager = panoramisk.Manager( + host = self.settings.get("asterisk-ami-host"), + username = self.settings.get("asterisk-ami-username"), + secret = self.settings.get("asterisk-ami-secret"), + + on_connect=self._on_connect, + ) + + # Connect + await manager.connect() + + return manager + + def _on_connect(self, manager): + logging.debug("Connection to Asterisk established") + + # Close any existing connections + if self.__manager: + self.__manager.close() + + self.__manager = manager + + async def ping(self): + manager = await self.manager() + + result = manager.ping() + print(result) + + async def get_sip_channels(self, filter=None): + channels = [] + + for data in await self.manager.send_action({"Action" : "CoreShowChannels"}): + # Skip header and trailer + if data.eventlist: + continue + + # Parse channel + channel = Channel(self.backend, data) + + # Apply filter + if filter and not channel.matches(filter): + continue + + channels.append(channel) + + return channels + + +class Channel(misc.Object): + def init(self, data): + self.data = data + + def __str__(self): + return self.connected_line + + @property + def account_code(self): + return self.data.AccountCode + + @property + def connected_line(self): + return self.data.ConnectedLineName or self.data.ConnectedLineNum + + def matches(self, filter): + return filter in ( + self.data.CallerIDNum, + ) + + @property + def duration(self): + h, m, s = self.data.Duration.split(":") + + try: + h, m, s = int(h), int(m), int(s) + except TypeError: + return 0 + + return datetime.timedelta(hours=h, minutes=m, seconds=s) + + def is_connected(self): + return self.data.ChannelStateDesc == "Up" + + def is_ringing(self): + return self.data.ChannelStateDesc == "Ringing" diff --git a/src/backend/base.py b/src/backend/base.py index 6ac600d1..ea0c0258 100644 --- a/src/backend/base.py +++ b/src/backend/base.py @@ -8,6 +8,7 @@ import tempfile import tornado.httpclient from . import accounts +from . import asterisk from . import blog from . import bugzilla from . import campaigns @@ -62,6 +63,7 @@ class Backend(object): # Initialize backend modules. self.accounts = accounts.Accounts(self) + self.asterisk = asterisk.Asterisk(self) self.bugzilla = bugzilla.Bugzilla(self) self.fireinfo = fireinfo.Fireinfo(self) self.iuse = iuse.IUse(self) diff --git a/src/backend/util.py b/src/backend/util.py index 9a4246d7..00747502 100644 --- a/src/backend/util.py +++ b/src/backend/util.py @@ -3,6 +3,7 @@ import PIL.Image import PIL.ImageFilter import PIL.ImageOps +import datetime import io import ipaddress import location @@ -171,6 +172,9 @@ def format_time(s, shorter=True): #_ = handler.locale.translate _ = lambda x: x + if isinstance(s, datetime.timedelta): + s = s.total_seconds() + hrs, s = divmod(s, 3600) min, s = divmod(s, 60) diff --git a/src/templates/users/show.html b/src/templates/users/show.html index 842f11ed..40a5ae65 100644 --- a/src/templates/users/show.html +++ b/src/templates/users/show.html @@ -136,6 +136,35 @@ {% if current_user == account or current_user.is_staff() %} + + {# SIP Channels #} + + {% if sip_channels %} +
+
+ {% for channel in sip_channels %} +
+ + + + + {{ channel }} + + + {% if channel.duration %} + + {{ format_time(channel.duration) }} + + {% end %} +
+ {% end %} +
+
+ {% end %} +
@@ -161,7 +190,7 @@
    {% if account.has_sip() %}
  • - {% if account.sip_channels %} + {% if sip_channels %} {% elif account.sip_registrations %} diff --git a/src/web/users.py b/src/web/users.py index 6dee34b9..7435a552 100644 --- a/src/web/users.py +++ b/src/web/users.py @@ -33,12 +33,15 @@ class IndexHandler(base.BaseHandler): class ShowHandler(base.BaseHandler): @tornado.web.authenticated - def get(self, uid): + async def get(self, uid): account = self.backend.accounts.get_by_uid(uid) if not account: raise tornado.web.HTTPError(404, "Could not find account %s" % uid) - self.render("users/show.html", account=account) + # Fetch SIP channels + sip_channels = await account.get_sip_channels() + + self.render("users/show.html", account=account, sip_channels=sip_channels) class AvatarHandler(base.BaseHandler): -- 2.39.2