#!/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$}" 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) return "0 Eintr" not in urllib2.urlopen(url).read() 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 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_local_blacklist(call.remote_address.username) or is_in_ktipp_blacklist(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 global_state_changed(*args, **kwargs): log("global_state_changed: %r %r" % (args, kwargs)) def registration_state_changed(core, call, state, message): log("registration_state_changed: " + str(state) + ", " + message) 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, 'registration_state_changed': registration_state_changed, 'global_state_changed': global_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"