]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/asterisk.py
wiki: Only match usernames when a word starts with @
[ipfire.org.git] / src / backend / asterisk.py
1 #!/usr/bin/python3
2
3 import asyncio
4 import datetime
5 import logging
6 import panoramisk
7 import urllib.parse
8
9 from . import accounts
10 from . import misc
11 from .decorators import *
12
13 # Make this less verbose
14 logging.getLogger("panoramisk").setLevel(logging.INFO)
15
16 class Asterisk(misc.Object):
17 def init(self):
18 self.__manager = None
19
20 loop = asyncio.get_event_loop()
21
22 # Connect as soon as the event loop starts
23 loop.create_task(self.connect())
24
25 @property
26 def manager(self):
27 if not self.__manager:
28 raise RuntimeError("Asterisk is not connected")
29
30 return self.__manager
31
32 async def connect(self):
33 """
34 Connects to Asterisk
35 """
36 manager = panoramisk.Manager(
37 host = self.settings.get("asterisk-ami-host"),
38 username = self.settings.get("asterisk-ami-username"),
39 secret = self.settings.get("asterisk-ami-secret"),
40
41 on_connect=self._on_connect,
42 )
43
44 # Connect
45 await manager.connect()
46
47 return manager
48
49 def _on_connect(self, manager):
50 logging.debug("Connection to Asterisk established")
51
52 # Close any existing connections
53 if self.__manager:
54 self.__manager.close()
55
56 self.__manager = manager
57
58 async def _fetch(self, cls, action, filter=None, data={}):
59 objects = []
60
61 # Collect arguments
62 args = { "Action" : action } | data
63
64 # Run the action and parse all messages
65 for data in await self.manager.send_action(args):
66 if not "Event" in data or not data.Event == cls.event:
67 continue
68
69 # Create the object and append it to the list
70 o = cls(self.backend, data)
71
72 # Filter out anything unwanted
73 if filter and not o.matches(filter):
74 continue
75
76 objects.append(o)
77
78 return objects
79
80 async def get_sip_channels(self, filter=None):
81 return await self._fetch(Channel, "CoreShowChannels", filter=filter)
82
83 async def get_registrations(self, filter=None):
84 return await self._fetch(Registration, "PJSIPShowContacts", filter=filter)
85
86 async def get_outbound_registrations(self):
87 return await self._fetch(OutboundRegistration, "PJSIPShowRegistrationsOutbound")
88
89 async def get_queues(self):
90 # Fetch all queues
91 queues = { q.name : q for q in await self._fetch(Queue, "QueueSummary") }
92
93 # Fetch all members
94 for member in await self._fetch(QueueMember, "QueueStatus"):
95 # Append to the matching queue
96 try:
97 queues[member.queue].members.append(member)
98 except KeyError:
99 pass
100
101 return queues.values()
102
103 async def get_conferences(self):
104 conferences = await self._fetch(Conference, "ConfbridgeListRooms")
105
106 # Fetch everything else
107 async with asyncio.TaskGroup() as tasks:
108 for c in conferences:
109 tasks.create_task(c._fetch())
110
111 return conferences
112
113
114 class Channel(misc.Object):
115 event = "CoreShowChannel"
116
117 def init(self, data):
118 self.data = data
119
120 def __str__(self):
121 return self.connected_line
122
123 @property
124 def account_code(self):
125 return self.data.AccountCode
126
127 @property
128 def connected_line(self):
129 return self.data.ConnectedLineName or self.data.ConnectedLineNum
130
131 def matches(self, filter):
132 return filter in (
133 self.data.CallerIDNum,
134 )
135
136 @property
137 def duration(self):
138 h, m, s = self.data.Duration.split(":")
139
140 try:
141 h, m, s = int(h), int(m), int(s)
142 except TypeError:
143 return 0
144
145 return datetime.timedelta(hours=h, minutes=m, seconds=s)
146
147 def is_connected(self):
148 return self.data.ChannelStateDesc == "Up"
149
150 def is_ringing(self):
151 return self.data.ChannelStateDesc == "Ringing"
152
153
154 class Registration(misc.Object):
155 event = "ContactList"
156
157 def init(self, data):
158 self.data = data
159
160 def __lt__(self, other):
161 if isinstance(other, self.__class__):
162 if isinstance(self.user, accounts.Account):
163 if isinstance(other.user, accounts.Account):
164 return self.user < other.user
165 else:
166 return self.user.name < other.user
167 else:
168 if isinstance(other.user, accounts.Account):
169 return self.user < other.user.name
170 else:
171 return self.user < other.user
172
173 return NotImplemented
174
175 def __str__(self):
176 return self.user_agent
177
178 def matches(self, filter):
179 return self.data.Endpoint == filter
180
181 @lazy_property
182 def uri(self):
183 return urllib.parse.urlparse(self.data.Uri)
184
185 @lazy_property
186 def uri_params(self):
187 params = {}
188
189 for param in self.uri.params.split(";"):
190 key, _, value = param.partition("=")
191
192 params[key] = value
193
194 return params
195
196 @property
197 def transport(self):
198 return self.uri_params.get("transport")
199
200 @lazy_property
201 def user(self):
202 return self.backend.accounts.get_by_sip_id(self.data.Endpoint) or self.data.Endpoint
203
204 @property
205 def address(self):
206 # Remove the user
207 user, _, address = self.uri.path.partition("@")
208
209 # Remove the port
210 address, _, port = address.rpartition(":")
211
212 return address
213
214 @property
215 def user_agent(self):
216 return self.data.UserAgent.replace("_", " ")
217
218 @property
219 def roundtrip(self):
220 try:
221 return int(self.data.RoundtripUsec) / 1000
222 except ValueError:
223 pass
224
225
226 class OutboundRegistration(misc.Object):
227 event = "OutboundRegistrationDetail"
228
229 def init(self, data):
230 self.data = data
231
232 def __lt__(self, other):
233 if isinstance(other, self.__class__):
234 return self.server < other.server or self.username < other.username
235
236 return NotImplemented
237
238 @lazy_property
239 def uri(self):
240 return urllib.parse.urlparse(self.data.ClientUri)
241
242 @property
243 def server(self):
244 username, _, server = self.uri.path.partition("@")
245
246 return server
247
248 @property
249 def username(self):
250 username, _, server = self.uri.path.partition("@")
251
252 return username
253
254 @property
255 def status(self):
256 return self.data.Status
257
258
259 class Queue(misc.Object):
260 event = "QueueSummary"
261
262 def init(self, data):
263 self.data = data
264
265 self.members = []
266
267 def __str__(self):
268 return self.name
269
270 @property
271 def name(self):
272 return self.data.Queue
273
274 def is_available(self):
275 return self.data.Available == "1"
276
277 @property
278 def callers(self):
279 return int(self.data.Callers)
280
281
282 class QueueMember(misc.Object):
283 event = "QueueMember"
284
285 def init(self, data):
286 self.data = data
287
288 def __str__(self):
289 return self.name
290
291 @property
292 def name(self):
293 return self.data.Name
294
295 @property
296 def queue(self):
297 return self.data.Queue
298
299 @property
300 def calls_taken(self):
301 return int(self.data.CallsTaken)
302
303 def is_in_call(self):
304 return self.data.InCall == "1"
305
306 @property
307 def last_call_at(self):
308 return datetime.datetime.fromtimestamp(int(self.data.LastCall))
309
310 @property
311 def logged_in_at(self):
312 return datetime.datetime.fromtimestamp(int(self.data.LoginTime))
313
314 # XXX status?
315
316
317 class Conference(misc.Object):
318 event = "ConfbridgeListRooms"
319
320 def init(self, data):
321 self.data = data
322
323 def __str__(self):
324 return self.name
325
326 @property
327 def name(self):
328 return self.data.Conference
329
330 async def _fetch(self):
331 # Fetch all members
332 self.members = await self.backend.asterisk._fetch(
333 ConferenceMember, "ConfbridgeList", data={ "Conference" : self.name, })
334
335
336 class ConferenceMember(misc.Object):
337 event = "ConfbridgeList"
338
339 def init(self, data):
340 self.data = data
341
342 def __str__(self):
343 return self.name
344
345 def __lt__(self, other):
346 if isinstance(other, self.__class__):
347 return not self.duration < other.duration
348
349 return NotImplemented
350
351 @property
352 def name(self):
353 return "%s <%s>" % (self.data.CallerIDName, self.data.CallerIDNum)
354
355 def is_admin(self):
356 return self.data.Admin == "Yes"
357
358 def is_muted(self):
359 return self.data.Muted == "Yes"
360
361 @property
362 def duration(self):
363 return datetime.timedelta(seconds=int(self.data.AnsweredTime))