123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- #!/usr/bin/env python2
- # -*- coding: utf-8 -*-
- import audioop
- import contextlib
- import glob
- import io
- import os
- import string
- import subprocess
- import syslog
- import threading
- import urllib2
- import wave
- from datetime import datetime
- import linphone
- from enum import Enum
- VOLUME_THRESHOLD = 100
- def slugify(s):
- """
- Normalizes string, converts to lowercase, removes non-alpha characters,
- and converts spaces to hyphens, wich is url/filename friendly.
- """
- valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)
- filename = ''.join(c for c in s if c in valid_chars)
- filename = filename.replace(' ', '_') # I don't like spaces in filenames.
- return filename
- class ConversationStatus(Enum):
- READY_TO_TALK = 0
- IMTALKING = 1
- WAITFORANSWER = 2
- class Conversation(object):
- def __init__(self):
- self._status = ConversationStatus.READY_TO_TALK
- @property
- def status(self):
- return self._status
- @status.setter
- def status(self, value):
- if value != self._status:
- self._status = value
- current_dir = os.path.dirname(os.path.realpath(__file__))
- replies_seq = glob.glob(current_dir + "/replies/sequence/*.wav")
- replies_seq.sort()
- replies_generic = glob.glob(current_dir + "/replies/generic/*.wav")
- replies_generic.sort()
- incoming_stream_file = "/tmp/lenny"
- conversation = Conversation()
- replies_pos = 0
- THREADS_MUST_QUIT = False
- KTIP_LOOKUP_URL = "https://www.ktipp.ch/service/warnlisten/detail/?warnliste_id=7&ajax=ajax-search-form&keyword={" \
- "$number$}"
- SHOULDIANSWER_LOOKUP_URL = "https://ch.shouldianswer.net/telefonnummer/{$number$}"
- def is_in_blacklists(a_number):
- the_number = a_number.lstrip("0")
- the_number = the_number.replace("+", "")
- return is_in_local_blacklist(the_number) or is_in_ktipp_blacklist(the_number) or is_in_shiansw_blacklist(the_number)
- def is_in_local_blacklist(a_number):
- black_list = current_dir + "/blacklist.txt"
- if os.path.isfile(black_list):
- return a_number in open(current_dir + "/blacklist.txt").read()
- def is_in_ktipp_blacklist(a_number):
- # On peut interroger le site ktipp:
- # https://www.ktipp.ch/service/warnlisten/detail/?warnliste_id=7&ajax=ajax-search-form&keyword=0445510503
- # Si argument keyword pas trouvé, ca donne ca dans la réponse :
- # 0 Einträge
- url = KTIP_LOOKUP_URL.replace("{$number$}", a_number)
- log("KTIP lookup : " + url)
- response = ""
- try:
- response = urllib2.urlopen(url).read()
- except urllib2.HTTPError:
- pass
- return "0 Eintr" not in response
- def is_in_shiansw_blacklist(a_number):
- url = SHOULDIANSWER_LOOKUP_URL.replace("{$number$}", a_number)
- log("SIANSW lookup : " + url)
- response = ""
- try:
- response = urllib2.urlopen(url).read()
- except urllib2.HTTPError:
- pass
- return '<div class="review_score negative"></div>' in response
- def log(msg):
- syslog.syslog(msg)
- print(msg)
- def get_wav_duration(fname):
- with contextlib.closing(wave.open(fname, 'r')) as f:
- frames = f.getnframes()
- rate = f.getframerate()
- return frames / float(rate)
- def sleep(duration):
- dummy_event = threading.Event()
- dummy_event.wait(timeout=duration)
- def say(core):
- global conversation
- global replies_pos
- if conversation.status is not ConversationStatus.IMTALKING:
- conversation.status = ConversationStatus.IMTALKING
- # On joue les repliques en sequence, puis quand
- # on arrive au bout, on en joue une au hasard
- # du groupe 'generic'
- voice_filename = replies_seq[replies_pos]
- replies_pos = (replies_pos + 1) % len(replies_seq)
- if replies_pos == 0:
- # On ne rejoue jamais la première réplique "allo"
- replies_pos = 1
- duration = get_wav_duration(voice_filename)
- log("Saying : " + voice_filename + "(duration : " + str(duration) + ")")
- core.play_file = voice_filename
- sleep(duration)
- core.play_file = ""
- # On laisse l'autre l'occassion de reparler
- conversation.status = ConversationStatus.WAITFORANSWER
- def call_state_changed(core, call, state, message):
- global conversation
- global replies_pos
- log("state changed : " + message)
- if state == linphone.CallState.Released:
- # Let's convert wav to mp3
- log("Converting output from wav to mp3")
- if call.current_params.record_file is not None and os.path.isfile(call.current_params.record_file):
- subprocess.call('lame --quiet --preset insane %s' % call.current_params.record_file, shell=True)
- os.remove(call.current_params.record_file)
- if state == linphone.CallState.IncomingReceived:
- log("Incoming call : {}".format(call.remote_address.username))
- replies_pos = 0
- if is_in_blacklists(call.remote_address.username):
- log("telemarketer calling : " + call.remote_address.username)
- call_params = core.create_call_params(call)
- a_file = current_dir + "/out/call_from_" + slugify(call.remote_address.username) + \
- "_" + datetime.now().strftime(
- '%Y-%m-%d_%Hh%Mmn%Ss') + ".wav"
- log(a_file)
- call_params.record_file = a_file
- # Let ring some time
- sleep(5)
- core.accept_call_with_params(call, call_params)
- call.start_recording()
- sleep(2)
- t = threading.Thread(target=incoming_stream_worker, args=[core, call])
- t.start()
- say(core)
- def incoming_stream_worker(core, call):
- global conversation
- log("Worker is starting")
- f = open(incoming_stream_file, "rb")
- f.seek(0, io.SEEK_END)
- p = f.tell()
- buf = ''
- previous_status = conversation.status
- while call.state is not linphone.CallState.End and not THREADS_MUST_QUIT:
- if conversation.status is ConversationStatus.IMTALKING:
- f.seek(0, io.SEEK_END)
- p = f.tell()
- else:
- if previous_status != conversation.status:
- f.seek(0, io.SEEK_END)
- p = f.tell()
- f.seek(p)
- buf += f.read(4096)
- p = f.tell()
- if len(buf) >= 20000:
- volume = audioop.rms(buf, 2)
- print("Detected volume : " + str(volume))
- # print("State : " + str(conversation.status))
- buf = ''
- if volume < VOLUME_THRESHOLD:
- if conversation.status is ConversationStatus.READY_TO_TALK:
- threading.Thread(target=say, args=[core]).start()
- else:
- conversation.status = ConversationStatus.READY_TO_TALK
- # We must sleep a bit to avoid cpu hog
- sleep(0.01)
- previous_status = conversation.status
- log("Worker is quitting")
- def main():
- log("lenny is starting ...")
- callbacks = {
- 'call_state_changed': call_state_changed
- }
- username = "621"
- password = "toto"
- port = "5060"
- domain = "192.168.1.1"
- core = linphone.Core.new(callbacks, None, None)
- # On fait le setup pour la capture et analyse du stream entrant
- os.system("rm -rf " + incoming_stream_file)
- os.system("touch " + incoming_stream_file)
- core.use_files = True
- core.record_file = incoming_stream_file
- proxy_cfg = core.create_proxy_config()
- proxy_cfg.identity_address = core.create_address('sip:' + username + '@' + domain + ':' + port)
- proxy_cfg.server_addr = 'sip:' + domain + ':' + port
- proxy_cfg.register_enabled = True
- core.add_proxy_config(proxy_cfg)
- auth_info = core.create_auth_info(username, None, password, None, None, domain)
- core.add_auth_info(auth_info)
- while True:
- sleep(0.03)
- core.iterate()
- if __name__ == "__main__":
- try:
- main()
- except KeyboardInterrupt:
- THREADS_MUST_QUIT = True
- print "Bye"
- # TODO : créer un fichier de config
- # TODO : pouvoir s'enregistrer sur plusieurs comptes SIP
- # TODO : Que le stream entrant utilise un fichier temporaire
- # TODO : De pouvoir utiliser des mp3 pour gagner de la place, qu'on convertit au vol en wav, puis qu'on efface.
- # TODO : Ecrire le readme
|