123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- #!/usr/bin/env python2
- # -*- coding: utf-8 -*-
- import ConfigParser
- import audioop
- import contextlib
- import glob
- import io
- import os
- import signal
- import string
- import subprocess
- import sys
- import syslog
- import tempfile
- 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(cc for cc in s if cc 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()
- 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):
- return is_in_local_blacklist(a_number) or is_in_ktipp_blacklist(a_number) or is_in_shiansw_blacklist(a_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
- the_number = a_number.lstrip("0")
- the_number = the_number.replace("+", "")
- url = KTIP_LOOKUP_URL.replace("{$number$}", the_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)
- class SipConnection(object):
- def say(self, core):
- if self._conversation.status is not ConversationStatus.IMTALKING:
- self._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[self._replies_pos]
- self._replies_pos = (self._replies_pos + 1) % len(replies_seq)
- if self._replies_pos == 0:
- # On ne rejoue jamais la première réplique "allo"
- self._replies_pos = 1
- duration = get_wav_duration(voice_filename)
- log("Saying : " + voice_filename)
- core.play_file = voice_filename
- sleep(duration)
- core.play_file = ""
- # On laisse l'autre l'occassion de reparler
- self._conversation.status = ConversationStatus.WAITFORANSWER
- def incoming_stream_worker(self, core, call):
- f = open(self._incoming_stream_file, "rb")
- f.seek(0, io.SEEK_END)
- p = f.tell()
- buf = ''
- previous_status = self._conversation.status
- while call.state is not linphone.CallState.End and not self._is_quitting:
- if self._conversation.status is ConversationStatus.IMTALKING:
- f.seek(0, io.SEEK_END)
- p = f.tell()
- else:
- if previous_status != self._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("State : " + str(conversation.status))
- buf = ''
- if volume < self._volume_threshold:
- if self._conversation.status is ConversationStatus.READY_TO_TALK:
- threading.Thread(target=self.say, args=[core]).start()
- else:
- self._conversation.status = ConversationStatus.READY_TO_TALK
- # We must sleep a bit to avoid cpu hog
- sleep(0.01)
- previous_status = self._conversation.status
- log("Worker is quitting")
- @staticmethod
- def registration_state_changed(core, call, state, message):
- log("Registration status: " + message)
- def call_state_changed(self, core, call, state, message):
- log("state changed : " + message)
- if state == linphone.CallState.Released:
- # Let's convert wav to mp3
- if call.current_params.record_file is not None and os.path.isfile(call.current_params.record_file):
- log("Saving to mp3 : " + 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))
- self._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)
- os.makedirs(current_dir + "/out")
- 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(4)
- core.accept_call_with_params(call, call_params)
- call.start_recording()
- sleep(2)
- t = threading.Thread(target=self.incoming_stream_worker, args=[core, call])
- t.start()
- self.say(core)
- def __enter__(self):
- return self
- def __exit__(self, exc_type, exc_value, traceback):
- log(str(self) + ": cleaning on exit ...")
- os.unlink(self._incoming_stream_file)
- def start(self):
- log("starting " + str(self))
- self._core.use_files = True
- self._core.record_file = self._incoming_stream_file
- proxy_cfg = self._core.create_proxy_config()
- proxy_cfg.identity_address = self._core.create_address('sip:' + self._username + '@' + self._domain + ':5060')
- proxy_cfg.server_addr = 'sip:' + self._domain + ':5060'
- proxy_cfg.register_enabled = True
- self._core.add_proxy_config(proxy_cfg)
- auth_info = self._core.create_auth_info(self._username, None, self._password, None, None, self._domain)
- self._core.add_auth_info(auth_info)
- while not self._is_quitting:
- sleep(0.03)
- self._core.iterate()
- def request_quit(self):
- self._is_quitting = True
- self.__exit__(None, None, None)
- def __str__(self):
- return self._username + "@" + self._domain
- def __init__(self, domain, username, password):
- callbacks = {
- 'call_state_changed': self.call_state_changed,
- 'registration_state_changed': self.registration_state_changed,
- }
- self._core = linphone.Core.new(callbacks, None, None)
- self._domain = domain
- self._username = username
- self._password = password
- self._is_quitting = False
- self._conversation = Conversation()
- self._replies_pos = 0
- self._volume_threshold = VOLUME_THRESHOLD
- self._incoming_stream_file = tempfile.NamedTemporaryFile(delete=False).name
- self._core.iterate()
- if __name__ == "__main__":
- cfg = ConfigParser.SafeConfigParser()
- cfg_path = current_dir + "/config.ini"
- if len(sys.argv) == 2:
- cfg_path = sys.argv[1]
- connections = []
- cfg.read(cfg_path)
- for c in cfg.sections():
- connections.append(SipConnection(cfg.get(c, "domain"), cfg.get(c, "username"), cfg.get(c, "password")))
- for sip_c in connections:
- threading.Thread(target=sip_c.start).start()
- # Ensuring clean quit and ressource releasing
- # when receiving ctrl-c from console or SIGTERM
- # from daemon manager.
- def signal_handler(sig, frame):
- print('External stop request!')
- for conn in connections:
- conn.request_quit()
- signal.signal(signal.SIGINT, signal_handler)
- signal.signal(signal.SIGTERM, signal_handler)
- signal.pause()
|