piphone.py 21.6 KB
Newer Older
okhin's avatar
okhin committed
1
2
3
#!/usr/bin/env python

import sqlite3
okhin's avatar
okhin committed
4
5
6
import datetime
import uuid
import json
7
8
9
import logging
import concurrent.futures
import asyncio
10
import re
11
12
import configparser
import argparse
13
14
import os.path
import sys
okhin's avatar
okhin committed
15
from operator import itemgetter
okhin's avatar
okhin committed
16
17

import jwt
18
import websockets
okhin's avatar
okhin committed
19
from bottle import request, abort, Bottle, JSONPlugin, template, static_file, auth_basic
20
from bottle_sqlite import SQLitePlugin
21
22

import ari
23

24
arg_parser = argparse.ArgumentParser(description='Manage the SIP Backend for the piphone')
25
config = configparser.ConfigParser()
26
27

arg_parser.add_argument('-c', '--config', help="Config file")
okhin's avatar
okhin committed
28
args = arg_parser.parse_args()
29
try:
okhin's avatar
okhin committed
30
    logging.debug("Let's use {} as a config file".format(args.config,))
okhin's avatar
okhin committed
31
    config.read(args.config)
okhin's avatar
okhin committed
32
33
except AttributeError as e:
    print(e)
34
35
36
37
    try:
        if os.path.isfile('config.ini'):
            logging.debug("Let's use config.ini as a config file")
            config.read('config.ini')
okhin's avatar
okhin committed
38
        elif os.path.isfile('/etc/piphone/sip_config.ini'):
39
            logging.debug("Let's use /etc/iphone/config.ini as a config file")
okhin's avatar
okhin committed
40
            config.read('/etc/piphone/sip_config.ini')
41
        else:
okhin's avatar
okhin committed
42
            raise Exception("No configuration file found (tried ./config.ini and /etc/piphone/sip_config.ini")
43
44
45
46
    except Exception as e:
        arg_parser.print_help()
        sys.exit(1)

47
application = app = Bottle(autojson=False)
48
app.install(SQLitePlugin(dbfile=config['piphone']['db']))
49
50
app.install(JSONPlugin(json_dumps=lambda s: json.dumps(s, cls=PiphoneJSONEncoder)))

51
threads = concurrent.futures.ThreadPoolExecutor(max_workers=5)
52
loop = asyncio.get_event_loop()
okhin's avatar
okhin committed
53

54
55
56
57
running = False
ws = None

# Loggers
58
59
handler = logging.FileHandler(config['piphone']['log'])
verbosity = getattr(logging, config['piphone']['verbosity'].upper()) or logging.DEBUG
60
61
phone_logger = logging.getLogger('piphone')
phone_logger.addHandler(handler)
62
phone_logger.setLevel(verbosity)
63
64
ws_logger = logging.getLogger('asterisk')
ws_logger.addHandler(handler)
65
ws_logger.setLevel(verbosity)
66
67
bottle_logger = logging.getLogger('bottle')
bottle_logger.addHandler(handler)
68
bottle_logger.setLevel(verbosity)
69
70
71
72
73
74
75
76
77
78

class PiphoneJSONEncoder(json.JSONEncoder):
    def default(self, obj):
        """
        We need to implement this to be able to JSONEncode
        """
        if isinstance(obj, Call):
            return {  'caller': obj.caller
                , 'callee': obj.callee
                , 'callid': obj.id
okhin's avatar
okhin committed
79
                , 'url': obj.url
80
81
82
                , 'history': obj.history
                , 'owner': obj.owner }
        else:
83
            return json.JSONEncoder.default(self, obj)
okhin's avatar
okhin committed
84

85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
def authenticated(f):
    '''
    We need a decorator to check if our query is authenticated.
    We will store an API key and SECRET in ur database, the client
    needs to have both of them.
    He must then send us a JWT token with an API claim in the payload.
    The JWT token must be encoded and signed with the SECRET. If the
    token is bad, we return a 403.
    '''
    def wrapped(db, *args, **kwargs):
        # Let's get the JWT token. It should be a params (from get or post or whatev')
        bottle_logger.debug("Authentication: {}".format([':'.join([key, request.params[key]]) for key in request.params],))
        if 'token' not in request.params:
            bottle_logger.error("No token found in the params")
            abort(401, "No token found in the query")
        # We want the api id in the params to.
        if 'api' not in request.params:
            bottle_logger.error("No api id found in the params")
            abort(401, "No api id found in the params")
        # Now, let's get the token on our side
        try:
            results = db.execute('SELECT token FROM users WHERE api = ?', (request.params['api'],)).fetchall()
            assert len(results) == 1
            token = results[0][0]
okhin's avatar
okhin committed
109
            auth_token = jwt.decode(request.params['token'], token)
110
            assert auth_token['api'] == request.params['api']
111
112
113
114
115
116
117
            for key in auth_token:
                request.params[key] = auth_token[key]
        except (jwt.exceptions.InvalidTokenError, AssertionError) as e:
            bottle_logger.error("Access refused")
            bottle_logger.exception(e)
            abort(403, e)
        except Exception as e:
okhin's avatar
okhin committed
118
            bottle_logger.exception(e)
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
            abort(500, e)
        return f(db, *args, **kwargs)
    return wrapped

def connected(f):
    '''
    This is a decorator used to check if we are connected to a websocket or not. If we're not
    We're returning 500 error.
    '''
    def wrapped(db, *args, **kwargs):
        if isinstance(ws, websockets.client.WebSocketClientProtocol):
            if ws.open == True:
                return f(db, *args, **kwargs)
        ws_logger.error("Websocket connection is closed")
        abort(500, "Websocket isn't running")
    return wrapped

okhin's avatar
okhin committed
136
137
138
139
140
141
142
143
144
145
146
147
def sanitize_phonenumber(number):
    """
    This function is used to sanitize a phone number.
    If it starts with a +, it will be removed and replaced by 00.
    Any chars who do notbelong to [0-9] will be stripped.
    If the number doesn't starts with 00, a TypeError will be raised
    """
    if number[0] == '+':
        number = '00' + number[1:]
    number = ''.join([c for c in number if c in '0123456789'])
    if not number.startswith('00'):
        raise TypeError('{} is not a valid international number, it should start with 00')
148
149
150
151

    # We're checking if we're blacklisted
    db = sqlite3.connect(config['piphone']['db'])
    bl_re = None
152
    for blacklist in db.execute('SELECT pattern, reason FROM blacklist').fetchall():
153
154
155
156
157
158
159
        if bl_re == None:
            bl_re ='(?P<{reason}>^{pattern})'.format(pattern=blacklist[0],reason=blacklist[1],)
        else:
            bl_re +='|(?P<{reason}>^{pattern})'.format(pattern=blacklist[0], reason=blacklist[1],)
    if bl_re != None:
        # We have blacklisted patterns, need to chck for them
        groups = re.match(bl_re, number)
160
        if groups is not None:
161
            # We matched, so we're blacklisted
okhin's avatar
okhin committed
162
163
            groupdict = groups.groupdict()
            raise ValueError('{} is blacklisted. Reason: {}', (number, list(groupdict)[0],))
okhin's avatar
okhin committed
164
165
    return number

okhin's avatar
okhin committed
166
@asyncio.coroutine
okhin's avatar
okhin committed
167
def listen(db):
168
169
170
171
172
    '''
    Start listening on the websocket
    '''
    global running
    global ws
173
    ws_logger.debug('Connecting to websocket: {}'.format(config['webservice']['base_url'] + '?app={}&api_key={}:{}'.format(config['asterisk']['app'], config['asterisk']['key'], config['asterisk']['password'])))
okhin's avatar
okhin committed
174
    ws = yield from websockets.connect(config['webservice']['base_url'] + '?app={}&api_key={}:{}'.format(
okhin's avatar
okhin committed
175
        config['asterisk']['app'], config['asterisk']['key'], config['asterisk']['password']))
176
177
178
    ws_logger.debug('Websocket connected: {}'.format(type(ws)))
    while running == True:
        try:
okhin's avatar
okhin committed
179
            event = yield from ws.recv()
180
            # Let's call the applications function
okhin's avatar
okhin committed
181
            yield from dispatch(json.loads(event), db)
182
183
184
185
186
187
188
189
190
191
192
        except websockets.exceptions.ConnectionClosed as e:
            ws_logger.warning("Connexion closed")
            ws_logger.exception(e)
            running = False
            ws.close()
            return
        except Exception as e:
            ws_logger.exception(e)
            continue
    ws.close()

okhin's avatar
okhin committed
193
@asyncio.coroutine
okhin's avatar
okhin committed
194
def dispatch(event, db):
195
196
197
198
199
200
201
202
203
    """
    Let's work on our events. Parse them and do request on the ARI API. Event is
    a dict loaded from JSON.
    """
    ws_logger.debug('Event received: {}'.format(event,))
    # Let's get the call ID, the call id isthe channel id minus the last -part.
    if 'channel' not in event:
        return
    call_id = re.sub('-\d+$', '', event['channel']['id'])
okhin's avatar
okhin committed
204
    call = Call.load(call_id, db)
205
206
    call.event_handler(event)

207
class Call(object):
okhin's avatar
okhin committed
208
    """
209
    This Class is used to manage operations on a call, to print it and dump it.
okhin's avatar
okhin committed
210
    """
211
    history = []
212
    actions = {'Created': 'call_caller'
213
        , 'ChannelStateChange': 'change'
214
        , 'ChannelDtmfReceived': 'dtmf'
215
        , 'ChannelDestroyed': 'hangup'
okhin's avatar
okhin committed
216
        , 'ChannelHangupRequest': 'hangup'}
okhin's avatar
okhin committed
217

218
    def __init__(self, caller, callee, owner, callid=None, db=None):
okhin's avatar
okhin committed
219
220
221
222
        try:
            self.caller = caller
            self.callee = callee
            self.owner = owner
223
            if callid == None:
224
                self.id = str(uuid.uuid4())
225
226
            else:
                self.id = callid
okhin's avatar
okhin committed
227
            self.db = db
okhin's avatar
okhin committed
228
        except Exception as e:
229
            phone_logger.exception(e)
okhin's avatar
okhin committed
230
231
            raise e

okhin's avatar
okhin committed
232
    @property
okhin's avatar
okhin committed
233
234
235
    def url(self):
        return ''.join(['/calls/', self.id])

okhin's avatar
okhin committed
236
    @property
okhin's avatar
okhin committed
237
238
239
240
    def state(self):
        sort = sorted(self.history, reverse=True, key=itemgetter(1))
        return sort[0][0]

okhin's avatar
okhin committed
241
242
    @classmethod
    def load(cls, callid, db):
okhin's avatar
okhin committed
243
        phone_logger.debug("Loading call {} from db {}".format(callid, db,))
okhin's avatar
okhin committed
244
        try:
245
            results = db.execute('SELECT caller, callee, owner, callid, history FROM calls WHERE callid = ?;', (callid,))
okhin's avatar
okhin committed
246
            result = results.fetchone()
247
            assert len(result) == 5
okhin's avatar
okhin committed
248
            object = cls(result[0], result[1], result[2], result[3], db=db)
okhin's avatar
okhin committed
249
250
            object.history = json.loads(result[4])
            return object
251
        except Exception as e:
252
            phone_logger.exception(e)
253
            raise e
okhin's avatar
okhin committed
254

255
    def update(self, new_state):
256
        '''
257
        Let's update the state of the call. new_state is a tuple in the form (newstate, timestamp,)
258
        '''
259
        phone_logger.debug("Got a new state: {}".format(new_state,))
260
261
        self.history.append(new_state)
        self.save()
262
263
264
265
266
267

    def event_handler(self, event):
        '''
        There's a new event related to our call
        '''
        state = event['type']
268
        if state in self.actions:
269
270
271
272
            try:
                getattr(self, self.actions[state])(event=event)
            except Exception as e:
                raise e
273

274
275
276
277
278
279
280
281
    def hangup(self, event):
        '''
        There's a call which has lost connection. Probably someone who hanged up the call.
        We need to check if the other side is still up, which can be done by checking
        the bridge item (it is in the channels part of the bridge object) and see if there's
        more than one channel.
        If there's more than one, then we need to send a hangup to the other side and then delete
        our channel, if not we need to delete ourselves and then delete the bridge.
282
        We might also be in a case where no channel has been created … or we're still on moh.
283
284
        '''
        bridge_id = '-'.join(event['channel']['id'].split('-')[:-1])
285
286
287
        self.update((':'.join([event['channel']['state']
            , event['channel']['id'].split('-')[-1]])
            , event['timestamp']))
288
289
        try:
            bridge = ari.Bridge(config['asterisk'], bridge_id, 'mixed')
290
            results = json.loads(bridge.status())
291
292
293
294
295
296
        except:
            # Not in a bridge yet. Our channel has been destroyed.
            # That or we're in moh.
            phone_logger.info('Channel destroyed {}'.format(event['channel']['id']))
            return

297
298
299
300
301
302
303
304
305
306
307
        if len(results['channels']) == 0:
            # We were the last channel standing
            phone_logger.info('Deleting bridge {}'.format(bridge_id))
            bridge.delete()
        else:
            # There's at least one channel standing
            for channel in results['channels']:
                chan = ari.Channel(config['asterisk'], channel)
                phone_logger.info('Hanging up channel {}'.format(channel))
                chan.delete()

okhin's avatar
okhin committed
308
309
310
311
312
    def dtmf(self, event):
        '''
        We received a DTMF sequence
        '''
        try:
okhin's avatar
okhin committed
313
            assert self.state.startswith('Up')
okhin's avatar
okhin committed
314
            # The only thing we want to do is to call the callee if we press 1
315
316
317
            if event['digit'] != '1':
                return
            # Now, we're connectig the other side
318
            # We need to originate a call to the other side
319
            phone_logger.info('Will now connect {} to {}'.format(self.caller, self.callee,))
320
            endpoint = 'SIP/' + sanitize_phonenumber(self.callee) + '@' + config['asterisk']['sip-context']
okhin's avatar
okhin committed
321
            channel = ari.Channel(config['asterisk'], self.id + '-' + sanitize_phonenumber(self.callee))
322
            channel.originate(endpoint)
okhin's avatar
okhin committed
323
        except AssertionError as e:
okhin's avatar
okhin committed
324
            phone_logger.error("Received a DTMF sequence out le being in  a '{}' state, ignoring: {}".format(self.state, event['digit']))
325
326
327
328
            raise e
        except ValueError as e:
            phone_logger.error("Incorrect number: {}".format(e.message,))
            raise e
okhin's avatar
okhin committed
329

330
331
332
333
    def change(self, event):
        '''
        Let's change the state of the call
        '''
okhin's avatar
okhin committed
334
        # First we need to check if it's really a change ie, if the new state is not the previous one
335
        self.update((':'.join([event['channel']['state']
okhin's avatar
okhin committed
336
            , event['channel']['id'].split('-')[-1]])
337
            , event['timestamp'],))
338
        phone_logger.info("New state for call {}: {}".format(event['channel']['id'], event['channel']['state']))
okhin's avatar
okhin committed
339
340
        # We now need to take action according to our new state
        if event['channel']['state'] == 'Up':
341
            # Are we the caller or the callee?
342
            if event['channel']['id'].endswith(sanitize_phonenumber(self.callee)):
okhin's avatar
okhin committed
343
                # We are the callee
okhin's avatar
okhin committed
344
                # Step 1 create a bridge
okhin's avatar
okhin committed
345
                bridge = ari.Bridge(config['asterisk'], self.id, 'mixing')
346
                phone_logger.debug("Creating a bridges to connect {} to {}".format(self.caller, self.callee,))
okhin's avatar
okhin committed
347
348
349
350
                try:
                    bridge.create()
                except Exception as e:
                    raise e
okhin's avatar
okhin committed
351
352
353
                # Step 2, stop playing moh to the channel
                channel = ari.Channel(config['asterisk'], self.id + '-' + sanitize_phonenumber(self.caller))
                channel.stopMoh()
354
                # Step 3, connect everyone
okhin's avatar
okhin committed
355
                channels = ",".join([self.id + '-' + sanitize_phonenumber(self.caller), event['channel']['id']])
356
                phone_logger.debug("Moving channels ({}) to the created bridge: {}".format(channels, bridge.name,))
357
                bridge.addChannel(channels)
358
                phone_logger.info("Call now fully connected: {} <-> {}".format(self.caller, self.callee))
359
                return
okhin's avatar
okhin committed
360
361
            # Call is being picked up, we want to play a song
            try:
okhin's avatar
okhin committed
362
                channel = ari.Channel(config['asterisk'], event['channel']['id'])
okhin's avatar
okhin committed
363
                channel.startMoh(config['moh']['class'])
okhin's avatar
okhin committed
364
            except Exception as e:
365
                phone_logger.exception(e)
okhin's avatar
okhin committed
366
                raise e
367
368

    def call_caller(self, event):
369
        '''
okhin's avatar
okhin committed
370
371
        Let's call the caller. It's a simple originate. We will also check if the MoH bridge is ready,
        because it will be used to store people in it waiting for the callee to pick up the phone.
372

okhin's avatar
okhin committed
373
        The bridge needed to connect the calls together will be created later.
374
        '''
375
        self.update((':'.join([event['type']
okhin's avatar
okhin committed
376
            , event['channel']['id'].split('-')[-1]])
377
            , event['timestamp'],))
378
        # Now, let's create the channel
379
        try:
okhin's avatar
okhin committed
380
381
            endpoint = 'SIP/' + sanitize_phonenumber(self.caller) + '@' + config['asterisk']['sip-context']
            channel = ari.Channel(config['asterisk'], self.id + '-' + sanitize_phonenumber(self.caller))
okhin's avatar
okhin committed
382
            channel.originate(endpoint)
383
        except Exception as e:
384
            phone_logger.exception(e)
385
            raise e
386
387

    def save(self):
388
389
390
        '''
        Save the Call to database.
        '''
391
        phone_logger.debug("Saving call {}: {}".format(self.id, json.dumps(self, cls=PiphoneJSONEncoder)))
392
393
394
395
396
397
        try:
            self.db.execute('''INSERT OR REPLACE INTO calls (caller, callee, owner, callid, history)
                VALUES (?, ?, ?, ?, ?) '''
                , (self.caller, self.callee, self.owner, self.id, json.dumps(self.history)))
            self.db.commit()
        except Exception as e:
okhin's avatar
okhin committed
398
            bottle_logger.exception(e)
399
            raise e
400

okhin's avatar
okhin committed
401
def start(db):
402
403
    global running
    running = True
404
    threads.submit(app.run, server='paste')
okhin's avatar
okhin committed
405
    loop.run_until_complete(listen(db))
406

407
408
def stop():
    global running
409
    running = False
410
411
412
    ws.close()
    loop.close()
    threads.shutdown(wait=False)
413
    sys.exit(0)
okhin's avatar
okhin committed
414

415
@app.get('/calls/<callid>')
416
@app.get('/calls/')
okhin's avatar
okhin committed
417
@authenticated
418
@connected
419
def calls(db, callid=None):
okhin's avatar
okhin committed
420
421
422
423
    """
    Return the list of calls associated to the connected user.
    The call has a status, caller, callee and history (status change+timestamp)
    """
424
    bottle_logger.debug("GET {}".format(request.fullpath))
425
426
427
428
429
430
    if callid == None:
        try:
            results = db.execute('SELECT callid FROM calls WHERE owner = ?;', (request.params['api'],))
            calls = []
            for row in results.fetchall():
                call = Call.load(row[0], db)
431
                bottle_logger.debug("Call fetched: {}".format(json.dumps(call, cls=PiphoneJSONEncoder)))
432
433
434
435
                calls.append(call)
            head = {'call': request.fullpath, 'user': request.params['api'], 'hits': len(calls)}
            return {'head': head, 'data': calls}
        except Exception as e:
436
            bottle_logger.exception(e)
437
438
439
440
441
442
            abort(500, "Exception")
    # We first need to check if we can access the callid we asked for
    try:
        results = db.execute('SELECT callid FROM calls WHERE owner = ? AND callid = ? ;'
                , (request.params['api'], callid,))
        rows = results.fetchall()
443
        bottle_logger.debug("Found {} results: {}".format(len(rows), rows))
444
445
        assert len(rows) == 1
        call = Call.load(callid, db)
okhin's avatar
okhin committed
446
        head = {'call': call.url, 'user': request.params['api'], 'hits': 1}
447
448
        return {'head': head, 'data': call}
    except AssertionError as e:
449
450
        bottle_logger.debug("Not exactly one results found, this is an issue")
        bottle_logger.error("Unauthorized access to call {} from user {}".format(callid, request.params['api']))
451
452
        abort(403, "You do not have the authorization to get this call")
    except Exception as e:
453
454
        bottle_logger.debug("Exception catched: {}".format(e,))
        bottle_logger.error("Call not found {}".format(callid,))
455
456
457
458
        abort(404, "This call does not exist")

@app.post('/calls/')
@app.post('/calls/<callid>')
459
@authenticated
460
@connected
461
def originate(db, callid=None):
462
    bottle_logger.debug("POST {}".format(request.fullpath))
463
464
    try:
        if callid is not None:
okhin's avatar
okhin committed
465
            call = Call(request.params['caller'], request.params['callee'], request.params['api'], callid=callid, db=db)
466
467
        else:
            call = Call(request.params['caller'], request.params['callee'], request.params['api'], db=db)
468
        bottle_logger.debug("Originate a call: {}".format(json.dumps(call, cls=PiphoneJSONEncoder)))
469
        call.event_handler({'type': 'Created', 'timestamp': datetime.datetime.now().isoformat(), 'channel': {'id': 'Init'}})
okhin's avatar
okhin committed
470
471
        call.save()
        head = {'call': call.url
472
473
            , 'user': request.params['api']
            , 'hits': 1}
474
        return {'header': head, 'data': call}
475
476
477
    except ValueError as e:
        bottle_logger.exception(e)
        abort(403, "Your phone number isnt' authorized to use the piphone")
478
    except Exception as e:
479
480
        bottle_logger.debug("Missing params : {}".format([p for p in request.params],))
        bottle_logger.exception(e)
481
        abort(400, "Missing or incorrect fields, the call cannot be processed")
482

483
484
485
486
487
488
489
490
@app.get('/static/<filepath:path>')
def static_files(filepath):
    """
    take care of static files.
    should use apache/nginx instead.
    """
    return static_file(filepath, root='./views')

okhin's avatar
okhin committed
491
def login_admin(user, password):
492
    db = sqlite3.connect(config['piphone']['db'])
okhin's avatar
okhin committed
493
494
495
    sql_user = db.execute('SELECT token, admin FROM users where api = ?', (user,))
    sql_user = sql_user.fetchone()
    if sql_user is None:
okhin's avatar
okhin committed
496
497
        # user does not exist
        return False
okhin's avatar
okhin committed
498
    if password != sql_user[0]:
okhin's avatar
okhin committed
499
500
        # password does not match
        return False
okhin's avatar
okhin committed
501
    if sql_user[1] == 0:
okhin's avatar
okhin committed
502
503
504
505
506
        # User is not admin
        return False
    return True


507
@app.get('/admin')
okhin's avatar
okhin committed
508
@auth_basic(login_admin)
509
510
def little_admin():
    db = sqlite3.connect(config['piphone']['db'])
511
    # Get the list of all users
512
    users = db.execute('SELECT api, token, admin FROM users').fetchall()
513
    # Get the list of all blacklist patterns
okhin's avatar
okhin committed
514
    blacklisted = db.execute('SELECT pattern, reason FROM blacklist').fetchall()
515
    return template('index', users=users, blacklists=blacklisted)
516
517

@app.post('/admin')
okhin's avatar
okhin committed
518
@auth_basic(login_admin)
519
520
def medium_admin():
    db = sqlite3.connect(config['piphone']['db'])
521
    api = request.forms.get('api')
522
    token = request.forms.get('api_token')
523
524
    admin = request.forms.get('admin')
    action = request.forms.get('action')
525
526
    pattern = request.forms.get('pattern')
    reason = request.forms.get('reason')
527
528
529
530
531
532
533
534
535
536

    if action == 'delete':
        db.execute("DELETE FROM users WHERE api = ?", (api, ))
        db.commit()
    elif action == 'add':
        db.execute("INSERT INTO users (api, token, admin) VALUES (?, ?, ?)", (api, token, admin))
        db.commit()
    elif action == 'update':
        db.execute("UPDATE users set token = ?, admin = ? where api = ?", (token, admin, api ))
        db.commit()
537
538
539
540
541
542
    elif action == 'blacklist':
        db.execute("INSERT INTO blacklist (pattern, reason) VALUES (?, ?)", (pattern, reason,))
        db.commit()
    elif action == 'whitelist':
        db.execute("DELETE FROM blacklist WHERE pattern = ?", (pattern,))
        db.commit()
543
544

    users = db.execute('SELECT api, token, admin FROM users').fetchall()
okhin's avatar
okhin committed
545
    blacklisted = db.execute('SELECT pattern, reason FROM blacklist').fetchall()
546
    return template('index', users=users, blacklists=blacklisted)
547
548


549
if __name__ == '__main__':
550
551
    db = sqlite3.connect(config['piphone']['db'])
    phone_logger.info("Starting the piphone SIP backend")
552
    try:
okhin's avatar
okhin committed
553
        start(db)
554
    except (KeyboardInterrupt, SystemExit):
555
        stop()