src/web/people.py \
src/web/ui_modules.py \
src/web/users.py \
+ src/web/voip.py \
src/web/wiki.py
webdir = $(backenddir)/web
templates_users_modulesdir = $(templates_usersdir)/modules
+templates_voip_DATA = \
+ src/templates/voip/index.html
+
+templates_voipdir = $(templatesdir)/voip
+
+templates_voip_modules_DATA = \
+ src/templates/voip/modules/registrations.html
+
+templates_voip_modulesdir = $(templates_voipdir)/modules
+
templates_wiki_DATA = \
src/templates/wiki/404.html \
src/templates/wiki/confirm-delete.html \
sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
- @lazy_property
- def sip_registrations(self):
- sip_registrations = []
+ # SIP Registrations
- for reg in self.backend.talk.freeswitch.get_sip_registrations(self.sip_url):
- reg.account = self
+ async def get_sip_registrations(self):
+ if not self.has_sip():
+ return []
- sip_registrations.append(reg)
+ return await self.backend.asterisk.get_registrations(self.sip_id)
- return sip_registrations
+ # SIP Channels
async def get_sip_channels(self):
if not self.has_sip():
import datetime
import logging
import panoramisk
+import urllib.parse
+from . import accounts
from . import misc
from .decorators import *
return channels
+ async def get_registrations(self, filter=None):
+ registrations = []
+
+ for data in await self.manager.send_action({"Action" : "PJSIPShowContacts"}):
+ # Skip header and trailer
+ if data.eventlist:
+ continue
+
+ # Parse registration
+ registration = Registration(self.backend, data)
+
+ # Apply filter
+ if filter and not registration.matches(filter):
+ continue
+
+ registrations.append(registration)
+
+ return registrations
+
class Channel(misc.Object):
def init(self, data):
def is_ringing(self):
return self.data.ChannelStateDesc == "Ringing"
+
+
+class Registration(misc.Object):
+ def init(self, data):
+ self.data = data
+
+ def __lt__(self, other):
+ if isinstance(other, self.__class__):
+ if isinstance(self.user, accounts.Account):
+ if isinstance(other.user, accounts.Account):
+ return self.user < other.user
+ else:
+ return self.user.name < other.user
+ else:
+ if isinstance(other.user, accounts.Account):
+ return self.user < other.user.name
+ else:
+ return self.user < other.user
+
+ return NotImplemented
+
+ def __str__(self):
+ return self.user_agent
+
+ def matches(self, filter):
+ return self.data.Endpoint == filter
+
+ @lazy_property
+ def uri(self):
+ return urllib.parse.urlparse(self.data.Uri)
+
+ @lazy_property
+ def uri_params(self):
+ params = {}
+
+ for param in self.uri.params.split(";"):
+ key, _, value = param.partition("=")
+
+ params[key] = value
+
+ return params
+
+ @property
+ def transport(self):
+ return self.uri_params.get("transport")
+
+ @lazy_property
+ def user(self):
+ return self.backend.accounts.get_by_sip_id(self.data.Endpoint) or self.data.Endpoint
+
+ @property
+ def address(self):
+ # Remove the user
+ user, _, address = self.uri.path.partition("@")
+
+ # Remove the port
+ address, _, port = address.rpartition(":")
+
+ return address
+
+ @property
+ def user_agent(self):
+ return self.data.UserAgent.replace("_", " ")
+
+ @property
+ def roundtrip(self):
+ return int(self.data.RoundtripUsec) / 1000
{% end %}
</div>
- <div class="column is-one-fifth">
+ <div class="column">
<ul>
<li>
<a href="/about">
</ul>
</div>
- <div class="column is-one-fifth">
+ <div class="column">
<ul>
<li>
<a href="/download">
</ul>
</div>
+ {% if current_user and current_user.is_staff() %}
+ <div class="column">
+ <ul>
+ <li>
+ <a href="/voip">
+ {{ _("VoIP") }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ {% end %}
+
<div class="column is-one-fifth">
<div class="block">
<a class="button is-primary is-fullwidth is-medium has-text-weight-bold is-uppercase"
<li>
{% if sip_channels %}
<i class="fas fa-phone-volume has-text-warning fa-fw"></i>
- {% elif account.sip_registrations %}
- <i class="fas fa-phone-square has-text-success fa-fw"></i>
{% elif account.uses_sip_forwarding() %}
<i class="fas fa-phone-square has-text-warning fa-fw"></i>
{% else %}
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("VoIP") }}{% end block %}
+
+{% block container %}
+ <section class="hero is-dark">
+ <div class="hero-body">
+ <div class="container">
+ <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+ <ul>
+ <li>
+ <a href="/">
+ {{ _("Home") }}
+ </a>
+ </li>
+ <li class="is-active">
+ <a href="#" aria-current="page">{{ _("VoIP") }}</a>
+ </li>
+ </ul>
+ </nav>
+
+ <h1 class="title is-1">{{ _("VoIP") }}</h1>
+ </div>
+ </div>
+ </section>
+
+ {% if registrations %}
+ <section class="section">
+ <div class="container">
+ <h4 class="title is-4">
+ {{ _("Registrations") }}
+ <span class="tag">{{ len(registrations) }}</span>
+ </h4>
+
+ {% module VoIPRegistrations(registrations) %}
+ </div>
+ </section>
+ {% end %}
+{% end block %}
--- /dev/null
+{% from ipfire.accounts import Account %}
+
+<table class="table is-striped is-fullwidth">
+ <thead>
+ <tr>
+ <th>{{ _("User") }}</th>
+ <th>{{ _("Transport") }}</th>
+ <th>{{ _("Address") }}</th>
+ <th>{{ _("User Agent") }}</th>
+ <th>{{ _("Latency") }}</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ {% for r in sorted(registrations) %}
+ <tr>
+ {# User #}
+ <th scope="row">
+ {% if isinstance(r.user, Account) %}
+ <a href="/users/{{ r.user.uid }}">{{ r.user }}</a>
+ {% else %}
+ {{ r.user }}
+ {% end %}
+ </th>
+
+ {# Transport #}
+ <td class="has-text-centered">
+ {{ r.transport }}
+ </td>
+
+ {# Address #}
+ <td>
+ {{ r.address }}
+ </td>
+
+ {# User Agent #}
+ <td>
+ {{ r.user_agent or _("N/A") }}
+ </td>
+
+ {# Latency #}
+ <td class="has-text-right">
+ {{ _("%.2fms") % r.roundtrip }}
+ </th>
+ </tr>
+ {% end %}
+ </tbody>
+</table>
from . import people
from . import ui_modules
from . import users
+from . import voip
from . import wiki
class Application(tornado.web.Application):
# Users
"UsersList" : users.ListModule,
+ # VoIP
+ "VoIPRegistrations" : voip.RegistrationsModule,
+
# Wiki
"WikiDiff" : wiki.WikiDiffModule,
"WikiList" : wiki.WikiListModule,
(r"/users/([a-z_][a-z0-9_-]{0,31})/edit", users.EditHandler),
(r"/users/([a-z_][a-z0-9_-]{0,31})/passwd", users.PasswdHandler),
+ # VoIP
+ (r"/voip", voip.IndexHandler),
+
# Static Pages
(r"/about", StaticHandler, { "template" : "about.html" }),
(r"/legal", StaticHandler, { "template" : "legal.html" }),
--- /dev/null
+#!/usr/bin/python3
+
+import asyncio
+import tornado.web
+
+from . import base
+from . import ui_modules
+
+class IndexHandler(base.BaseHandler):
+ @tornado.web.authenticated
+ async def get(self):
+ registrations, = await asyncio.gather(
+ self.backend.asterisk.get_registrations(),
+ )
+
+ self.render("voip/index.html", registrations=registrations)
+
+
+class RegistrationsModule(ui_modules.UIModule):
+ def render(self, registrations):
+ return self.render_string("voip/modules/registrations.html",
+ registrations=registrations)