예제 #1
0
파일: callblocker.py 프로젝트: mvo5/a1fbox
class CallBlocker:
    """ Parse call monitor, examine RING event's phone number. """

    def __init__(self, fc,
                 whitelist_pbids, blacklist_pbids, blocklist_pbid, blockname_prefix='',
                 min_score=6, min_comments=3,
                 block_abroad=False, block_illegal_prefix=True,
                 logger=None):
        """ Provide a whitelist phonebook (normally first index 0) and where blocked numbers should go into. """
        self.whitelist_pbids = whitelist_pbids
        self.blacklist_pbids = blacklist_pbids
        self.blocklist_pbid = blocklist_pbid
        self.blockname_prefix = blockname_prefix
        self.min_score = int(min_score)
        self.min_comments = int(min_comments)
        # self.block_anon = block_anon  # How should that work? Impossible?
        self.block_abroad = block_abroad
        self.block_illegal_prefix = block_illegal_prefix
        self.logger = logger
        print("Retrieving data from Fritz!Box..")
        self.pb = Phonebook(fc=fc)
        fritz_model = self.pb.fc.modelname
        fritz_os = self.pb.fc.system_version
        self.cp = CallPrefix(fc=self.pb.fc)
        self.pb.ensure_pb_ids_valid(self.whitelist_pbids + self.blacklist_pbids + [self.blocklist_pbid])
        self.reload_phonebooks()
        if self.cp.country_code != '0049':
            log.warning('This script was developed for usage in Germany - please contact the author!')
        print(f'Call blocker initialized.. '
              f'model:{fritz_model} ({fritz_os}) '
              f'country:{self.cp.country_code_name} ({self.cp.country_code}) '
              f'area:{self.cp.area_code_name} ({self.cp.area_code}) '
              f'whitelisted:{len(self.whitelist)} blacklisted:{len(self.blacklist)} prefixes:{len(self.cp.prefix_dict)}')
        if TELEGRAM_BOT_URL:
            requests.get(TELEGRAM_BOT_URL + quote("CallBlocker: initialized"))

    def reload_phonebooks(self):
        """ Whitelist should be reloaded e.g. every day, blacklist after each entry added. """
        self.whitelist = self.pb.get_all_numbers_for_pb_ids(self.whitelist_pbids)
        self.blacklist = self.pb.get_all_numbers_for_pb_ids(self.blacklist_pbids)
        self.list_age = time()

    def parse_and_examine_line(self, raw_line):
        """ Parse call monitor line, if RING event not in lists, rate and maybe block the number. """
        if time() - self.list_age >= 3600:  # Reload phonebooks if list is outdated
            self.reload_phonebooks()
        log.debug(raw_line)
        cm_line = CallMonitorLine(raw_line)
        print(cm_line)

        # New: also examine calls from inside to outside (CallMonitorType.CALL)
        if cm_line.type in [CallMonitorType.RING.value, CallMonitorType.CALL.value]:
            dt = cm_line.datetime  # Use same datetime for exact match

            if cm_line.type == CallMonitorType.RING.value:
                number = cm_line.caller  # Incoming call
            else:
                number = cm_line.callee  # Outgoing call

            if not number:  # Caller uses NO number (so called CLIR feature)

                # We cannot block anon numbers, except you could add a rule in Fritzbox to do so?
                rate = CallBlockerRate.PASS.value  # CallBlockerRate.BLOCK.value if self.block_anon else CallBlockerRate.PASS.value
                raw_line = f'{dt};{rate};0;;ANON;' + "\n"

            else:  # Caller WITH phone number

                is_abroad = number.startswith('00') and not number.startswith(self.cp.country_code)

                if number.startswith('0'):
                    full_number = number  # Number with either country code or area code
                else:
                    full_number = self.cp.area_code + number  # Number in same area network

                # 1. Is either full number 071..123... or short number 123... in the white- or blacklist?
                name_white = self.pb.get_name_for_number_in_dict(number, self.whitelist, area_code=self.cp.area_code)
                name_black = self.pb.get_name_for_number_in_dict(number, self.blacklist, area_code=self.cp.area_code)

                if name_white and name_black:
                    raise Exception(f'Problem in your phonebooks detected: '
                                    f'a number should not be on white- and blacklist. Please fix! Details: '
                                    f'whitelist:{name_white} blacklist:{name_black}')

                if name_white or name_black:
                    name = name_black if name_black else name_white  # Reason: black might win over white by blocking it
                    rate = CallBlockerRate.BLACKLIST.value if name_black else CallBlockerRate.WHITELIST.value
                    raw_line = f'{dt};{rate};0;{full_number};"{name}";' + "\n"

                else:
                    ci = CallInfo(full_number)
                    ci.get_cascade_score()

                    # ToDo: check also if e.g. the prefix is inactive, e.g. DE_LANDLINE_INACTIVE
                    # Is the prefix (Vorwahl) valid, existing country code OR area code?
                    prefix_name = self.cp.get_prefix_name(full_number)
                    if not prefix_name and not number.startswith('00'):  # Do not block e.g. Inmarsat or similar
                        prefix_name = FAKE_PREFIX

                    # If there is no other information name at least the country or area
                    if ci.name == UNKNOWN_NAME:
                        ci.name = prefix_name

                    # Adapt to logging style of call monitor. Task of logger to parse the values to keys/names?
                    score_str = f'"{ci.name}";{ci.score};{ci.comments};{ci.searches};'

                    # Bad code style here - should be rewritten soon
                    if (self.block_illegal_prefix and prefix_name == FAKE_PREFIX) \
                            or (self.block_abroad and is_abroad) \
                            or (ci.score >= self.min_score and ci.comments >= self.min_comments):
                        name = self.blockname_prefix + ci.name
                        # Precaution: should only happen if this is a call from outside, not from inside
                        if cm_line.type == CallMonitorType.RING.value:
                            # ToDo: should go in extra method
                            result = self.pb.add_contact(self.blocklist_pbid, name, full_number)
                            if result:  # If not {} returned, it's an error
                                log.warning("Adding to phonebook failed:")
                                print(result)
                            else:
                                # Reload phonebook to prevent re-adding number for next ring event
                                # self.blacklist = self.pb.get_all_numbers_for_pb_ids(self.blacklist_pbids)
                                self.reload_phonebooks()
                            rate = CallBlockerRate.BLOCK.value
                        else:
                            rate = CallBlockerRate.PASS.value
                    else:
                        rate = CallBlockerRate.PASS.value
                    raw_line = f'{dt};{rate};1;{full_number};{score_str}' + "\n"

            log.debug(raw_line)
            parsed_line = CallBlockerLine(raw_line)
            print(parsed_line)
            if self.logger:
                self.logger(raw_line)

            # ToDo: add that an event is only posted once if same full_number and e.g. not same minute
            if TELEGRAM_BOT_URL:
                requests.get(TELEGRAM_BOT_URL + quote("CallBlocker: " + raw_line))