class CallAttendant(object): """The CallAttendant provides call logging and call screening services.""" def __init__(self, config): """ The constructor initializes and starts the Call Attendant. :param config: the application config dict """ # The application-wide configuration self.config = config # Open the database db_path = None if self.config["TESTING"]: self.db = sqlite3.connect(":memory:") else: self.db = sqlite3.connect( os.path.join(self.config['ROOT_PATH'], self.config['DATABASE'])) # Create a synchronized queue for incoming callers from the modem self._caller_queue = queue.Queue() # Screening subsystem self.logger = CallLogger(self.db, self.config) self.screener = CallScreener(self.db, self.config) # Hardware subsystem # Create the modem with the callback functions that it invokes # when incoming calls are received. self.modem = Modem(self.config, self.phone_ringing, self.handle_caller) # Initialize the visual indicators (LEDs) self.approved_indicator = ApprovedIndicator() self.blocked_indicator = BlockedIndicator() self.ring_indicator = RingIndicator() # Start the User Interface subsystem (Flask) # Skip if we're running functional tests, because when testing # we use a memory database which can't be shared between threads. if not self.config["TESTING"]: print("Staring the Flask webapp") webapp.start(config) def handle_caller(self, caller): """ A callback function used by the modem that places the given caller object into the synchronized queue for processing by the run method. :param caller: a dict object with caller ID information """ if self.config["DEBUG"]: print("Adding to caller queue:") pprint(caller) self._caller_queue.put(caller) def phone_ringing(self, enabled): """ A callback fucntion used by the modem to signal if the phone is ringing. It controls the phone ringing status indicator. :param enabled: If True, signals the phone is ringing """ if enabled: self.ring_indicator.turn_on() else: self.ring_indicator.turn_off() def run(self): """ Processes incoming callers by logging, screening, blocking and/or recording messages. """ # Get relevant config settings root_path = self.config['ROOT_PATH'] screening_mode = self.config['SCREENING_MODE'] # Get configuration subsets block = self.config.get_namespace("BLOCK_") blocked = self.config.get_namespace("BLOCKED_") voice_mail = self.config.get_namespace("VOICE_MAIL_") # Build some common paths blocked_greeting_file = os.path.join(root_path, blocked['greeting_file']) general_greeting_file = os.path.join(root_path, voice_mail['greeting_file']) goodbye_file = os.path.join(root_path, voice_mail['goodbye_file']) invalid_response_file = os.path.join( root_path, voice_mail['invalid_response_file']) leave_message_file = os.path.join(root_path, voice_mail['leave_message_file']) voice_mail_menu_file = os.path.join(root_path, voice_mail['menu_file']) message_path = os.path.join(root_path, voice_mail["message_folder"]) # Ensure the message path exists if not os.path.exists(message_path): os.makedirs(message_path) # Instruct the modem to start feeding calls into the caller queue self.modem.handle_calls() # Process incoming calls while 1: try: # Wait (blocking) for a caller caller = self._caller_queue.get() # Perform the call screening caller_permitted = False caller_blocked = False # Check the whitelist if "whitelist" in screening_mode: print("Checking whitelist(s)") if self.screener.is_whitelisted(caller): permitted_caller = True caller["ACTION"] = "Permitted" self.approved_indicator.turn_on() # Now check the blacklist if not preempted by whitelist if not caller_permitted and "blacklist" in screening_mode: print("Checking blacklist(s)") if self.screener.is_blacklisted(caller): caller_blocked = True caller["ACTION"] = "Blocked" self.blocked_indicator.turn_on() if not caller_permitted and not caller_blocked: caller["ACTION"] = "Screened" # Log every call to the database call_no = self.logger.log_caller(caller) # Apply the configured actions to blocked callers if caller_blocked: # Build the filename for a potential message message_file = os.path.join( message_path, "{}_{}_{}_{}.wav".format( call_no, caller["NMBR"], caller["NAME"].replace('_', '-'), datetime.now().strftime("%m%d%y_%H%M"))) # Go "off-hook" # - Acquires a lock on the modem # - MUST be followed by hang_up() if self.modem.pick_up(): try: # Play greeting if "greeting" in blocked["actions"]: self.modem.play_audio(blocked_greeting_file) # Record message if "record_message" in blocked["actions"]: self.modem.play_audio(leave_message_file) self.modem.record_audio(message_file) # Enter voice mail elif "voice_mail" in blocked["actions"]: tries = 0 while tries < 3: self.modem.play_audio(voice_mail_menu_file) digit = self.modem.wait_for_keypress(5) if digit == '1': # Leave a message self.modem.play_audio( leave_message_file) self.modem.record_audio(message_file) time.sleep(1) self.modem.play_audio(goodbye_file) break elif digit == '0': # End this call self.modem.play_audio(goodbye_file) break elif digit == '': # Timeout break else: # Try again self.modem.play_audio( invalid_response_file) tries += 1 finally: # Go "on-hook" self.modem.hang_up() except Exception as e: print(e) return 1
class CallAttendant(object): """The CallAttendant provides call logging and call screening services.""" def __init__(self, config): """ The constructor initializes and starts the Call Attendant. :param config: the application config dict """ # The application-wide configuration self.config = config # Thread synchonization object self._stop_event = threading.Event() # Open the database if self.config["TESTING"]: self.db = sqlite3.connect(":memory:") else: self.db = sqlite3.connect(self.config['DB_FILE']) # Create a synchronized queue for incoming callers from the modem self._caller_queue = queue.Queue() # Initialize the visual indicators (LEDs) self.approved_indicator = ApprovedIndicator( self.config.get("GPIO_LED_APPROVED_PIN"), self.config.get("GPIO_LED_APPROVED_BRIGHTNESS", 100)) self.blocked_indicator = BlockedIndicator( self.config.get("GPIO_LED_BLOCKED_PIN"), self.config.get("GPIO_LED_BLOCKED_BRIGHTNESS", 100)) # Screening subsystem self.logger = CallLogger(self.db, self.config) self.screener = CallScreener(self.db, self.config) # Hardware subsystem # Create (and open) the modem self.modem = Modem(self.config) # Messaging subsystem self.voice_mail = VoiceMail(self.db, self.config, self.modem) # Start the User Interface subsystem (Flask) # Skip if we're running functional tests, because when testing # we use a memory database which can't be shared between threads. if not self.config["TESTING"]: print("Starting the Flask webapp") webapp.start(self.config) def handle_caller(self, caller): """ A callback function used by the modem that places the given caller object into the synchronized queue for processing by the run method. :param caller: a dict object with caller ID information """ if self.config["DEBUG"]: print("Adding to caller queue:") pprint(caller) self._caller_queue.put(caller) def run(self): """ Processes incoming callers by logging, screening, blocking and/or recording messages. :returns: exit code 1 on error otherwise 0 """ # Get relevant config settings screening_mode = self.config['SCREENING_MODE'] blocked = self.config.get_namespace("BLOCKED_") screened = self.config.get_namespace("SCREENED_") permitted = self.config.get_namespace("PERMITTED_") blocked_greeting_file = blocked['greeting_file'] screened_greeting_file = screened['greeting_file'] permitted_greeting_file = permitted['greeting_file'] # Instruct the modem to start feeding calls into the caller queue self.modem.start(self.handle_caller) # If testing, allow queue to be filled before processing for clean, readable logs if self.config["TESTING"]: time.sleep(1) # Process incoming calls exit_code = 0 caller = {} print("Waiting for call...") while not self._stop_event.is_set(): try: # Wait (blocking) for a caller try: caller = self._caller_queue.get(True, 3.0) except queue.Empty: continue # An incoming call has occurred, log it number = caller["NMBR"] print("Incoming call from {}".format(number)) # Vars used in the call screening caller_permitted = False caller_screened = False caller_blocked = False action = "" reason = "" # Check the whitelist if "whitelist" in screening_mode: print("> Checking whitelist(s)") is_whitelisted, reason = self.screener.is_whitelisted( caller) if is_whitelisted: caller_permitted = True action = "Permitted" self.approved_indicator.blink() # Now check the blacklist if not preempted by whitelist if not caller_permitted and "blacklist" in screening_mode: print("> Checking blacklist(s)") is_blacklisted, reason = self.screener.is_blacklisted( caller) if is_blacklisted: caller_blocked = True action = "Blocked" self.blocked_indicator.blink() if not caller_permitted and not caller_blocked: caller_screened = True action = "Screened" # Log every call to the database (and console) call_no = self.logger.log_caller(caller, action, reason) print("--> {} {}: {}".format(number, action, reason)) # Gather the data used to answer the call if caller_permitted: actions = permitted["actions"] greeting = permitted_greeting_file rings_before_answer = permitted["rings_before_answer"] elif caller_screened: actions = screened["actions"] greeting = screened_greeting_file rings_before_answer = screened["rings_before_answer"] elif caller_blocked: actions = blocked["actions"] greeting = blocked_greeting_file rings_before_answer = blocked["rings_before_answer"] # Wait for the callee to answer the phone, if configured to do so ok_to_answer = True ring_count = 1 # Already had at least 1 ring to get here while ring_count < rings_before_answer: # In North America, the standard ring cadence is "2-4", or two seconds # of ringing followed by four seconds of silence (33% Duty Cycle). if self.modem.ring_event.wait(10.0): ring_count = ring_count + 1 print(" > > > Ring count: {}".format(ring_count)) else: # wait timeout; assume ringing has stopped before the ring count # was reached because either the callee answered or caller hung up. ok_to_answer = False print( " > > > Ringing stopped: Caller hung up or callee answered" ) break # Answer the call! if ok_to_answer and len(actions) > 0: self.answer_call(actions, greeting, call_no, caller) print("Waiting for next call...") except KeyboardInterrupt: print("** User initiated shutdown") self._stop_event.set() except Exception as e: pprint(e) print("** Error running callattendant") self._stop_event.set() exit_code = 1 return exit_code def shutdown(self): print("Shutting down...") print("-> Stopping modem") self.modem.stop() print("-> Stopping voice mail") self.voice_mail.stop() print("-> Releasing resources") self.approved_indicator.close() self.blocked_indicator.close() def answer_call(self, actions, greeting, call_no, caller): """ Answer the call with the supplied actions, e.g, voice mail, record message, or simply pickup and hang up. :param actions: A tuple containing the actions to take for this call :param greeting: The wav file to play to the caller upon answering :param call_no: The unique call number identifying this call :param caller: The caller ID data """ # Go "off-hook" - Acquires a lock on the modem - MUST follow with hang_up() if self.modem.pick_up(): try: # Play greeting if "greeting" in actions: print(">> Playing greeting...") self.modem.play_audio(greeting) # Record message if "record_message" in actions: print(">> Recording message...") self.voice_mail.record_message(call_no, caller) # Enter voice mail menu elif "voice_mail" in actions: print(">> Starting voice mail...") self.voice_mail.voice_messaging_menu(call_no, caller) except RuntimeError as e: print("** Error handling a blocked caller: {}".format(e)) finally: # Go "on-hook" self.modem.hang_up()
class CallAttendant(object): """The CallAttendant provides call logging and call screening services.""" def handler_caller(self, caller): """Places the caller record in synchronized queue for processing""" self._caller_queue.put(caller) def phone_ringing(self, enabled): """Controls the phone ringing status indicator.""" if enabled: self.ring_indicator.turn_on() else: self.ring_indicator.turn_off() def __init__(self): """The constructor initializes and starts the Call Attendant""" self.settings = {} self.settings[ "db_name"] = "callattendant.db" # SQLite3 DB to store incoming call log, whitelist and blacklist self.settings[ "screening_mode"] = "whitelist_and_blacklist" # no_screening, whitelist_only, whitelist_and_blacklist, blacklist_only self.settings["bad_cid_patterns"] = "" # regex name patterns to ignore self.settings["ignore_private_numbers"] = False # Ignore "P" CID names self.settings["ignore_unknown_numbers"] = True # Ignore "O" CID names self.settings["block_calls"] = True self.db = sqlite3.connect(self.settings["db_name"]) # The current/last caller id self._caller_queue = Queue() # Visual indicators (LEDs) self.approved_indicator = ApprovedIndicator() self.blocked_indicator = BlockedIndicator() self.ring_indicator = RingIndicator() # Telephony subsystems self.logger = CallLogger(self.db) self.screener = CallScreener(self.db) self.modem = Modem(self) self.modem.handle_calls() # User Interface subsystem webapp.start() # Run the app while 1: """Processes incoming callers with logging and screening.""" # Wait (blocking) for a caller caller = self._caller_queue.get() # Perform the call screening mode = self.settings["screening_mode"] whitelisted = False blacklisted = False if mode in ["whitelist_only", "whitelist_and_blacklist"]: print "Checking whitelist(s)" if self.screener.is_whitelisted(caller): whitelisted = True caller["NOTE"] = "Whitelisted" self.approved_indicator.turn_on() if not whitelisted and mode in [ "blacklist_only", "whitelist_and_blacklist" ]: print "Checking blacklist(s)" if self.screener.is_blacklisted(caller): blacklisted = True caller["NOTE"] = "Blacklisted" self.blocked_indicator.turn_on() if self.settings["block_calls"]: #~ self.modem.play_audio("sample.wav") #~ self.modem.hang_up() self.modem.block_call() # Log every call to the database self.logger.log_caller(caller)