diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e9c110e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +*~ +docker-compose.yml diff --git a/.gitignore b/.gitignore index 34544ef..338f57d 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,6 @@ cython_debug/ # matrix-commander /store credentials.json + +# emacs +*~ diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..22330e4 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,35 @@ +# syntax=docker/dockerfile:1 + +FROM ubuntu:latest + +ENV TZ=Europe/Berlin +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +WORKDIR /app +VOLUME /app + +COPY . . + +RUN ls -la +RUN ["cat", "/app/requirements.txt"] + +RUN /bin/bash -c 'apt update && apt install -y libolm-dev python3-pip ffmpeg;' + + +RUN pip install update pip && pip install -r requirements.txt + + +CMD python3 fritzab2matrix.py && tail -f /dev/null + + + + + + + + + + + + + diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..421eea3 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,16 @@ +version: "3.7" + +services: + app: + container_name: "fritzab2matrix" + build: + context: ../. + dockerfile: ./docker/Dockerfile + working_dir: /app + volumes: + - ./:/app + + + + + diff --git a/fritzab2matrix.py b/fritzab2matrix.py index 87946c4..0278d7f 100644 --- a/fritzab2matrix.py +++ b/fritzab2matrix.py @@ -1,14 +1,13 @@ - from fritzconnection import FritzConnection from dotenv import load_dotenv from pydub import AudioSegment +from libs.monitoring import endedCall +from libs.message import conversion as conv import urllib.request import xmltodict import sys, os import smbclient -import ctypes -import wave @@ -17,179 +16,106 @@ load_dotenv() env_user = os.environ.get('FRITZ_USERNAME') env_pass = os.environ.get('FRITZ_PASSWORD') env_ip = os.environ.get('FRITZ_IP') +env_tmp = os.environ.get('TEMP_DIR') +if env_tmp is None: + env_tmp = "/tmp" -### CHECK AND GET MESSAGES FROM FRITZBOX ### -############################################ +def main(): -## Connect to the FritzBox in the LAN -# We don't use tls because the self-signed cert of the box leads to a malfunction in urllib later on. -fc = FritzConnection(address=env_ip, user=env_user, password=env_pass, use_tls=False) + ### CHECK AND GET MESSAGES FROM FRITZBOX ### + ############################################ + + ## Connect to the FritzBox in the LAN + # We don't use tls because the self-signed cert of the box leads to a malfunction in urllib later on. + fc = FritzConnection(address=env_ip, user=env_user, password=env_pass, use_tls=False) -## Get info about messages from the main answering machine -message_list = fc.call_action("X_AVM-DE_TAM1", "GetMessageList", NewIndex=0) -message_list_url = message_list['NewURL'] + ## Get info about messages from the main answering machine + message_list = fc.call_action("X_AVM-DE_TAM1", "GetMessageList", NewIndex=0) + message_list_url = message_list['NewURL'] -### Convert fb speex format to wav ### -###################################### -# c&p that from https://git.savannah.nongnu.org/cgit/fbvbconv-py.git/tree/fbvbconv.py + # Build the url to download the message via smb + def build_download_url(mid, tam=0): + url = r"//" + env_ip + r"/fritz.nas/FRITZ/voicebox/rec/rec." + str(tam) + r"." + str(mid).zfill(3) + return url -speexlib = ctypes.cdll.LoadLibrary("libspeex.so.1") -SPEEX_SET_SAMPLING_RATE = 24 -SPEEX_GET_FRAME_SIZE = 3 - -class SpeexMode(ctypes.c_void_p): - pass -class Speex(ctypes.c_void_p): - pass -class SpeexBits(ctypes.Structure): - _fields_ = [ - ('chars', ctypes.c_char_p), - ('nbBits', ctypes.c_int), - ('charPtr', ctypes.c_int), - ('bitPtr', ctypes.c_int), - ('owner', ctypes.c_int), - ('overflow', ctypes.c_int), - ('but_size', ctypes.c_int), - ('reserved1', ctypes.c_int), - ('reserved2', ctypes.c_void_p) - ] - -speex_lib_get_mode = speexlib.speex_lib_get_mode -speex_lib_get_mode.restype = SpeexMode -speex_decoder_init = speexlib.speex_decoder_init -speex_decoder_init.restype = Speex -speex_decoder_ctl = speexlib.speex_decoder_ctl -speex_bits_init = speexlib.speex_bits_init -speex_bits_read_from = speexlib.speex_bits_read_from -speex_decode_int = speexlib.speex_decode_int -speex_bits_remaining = speexlib.speex_bits_remaining -speex_bits_destroy = speexlib.speex_bits_destroy - - -def speex_convert(inp, outp): -# rec = open(inp, 'rb').read() - rec = inp.read() - wav = wave.open(outp, 'wb') - wav.setnchannels(1) - wav.setsampwidth(2) - wav.setframerate(8000) - - mode = speex_lib_get_mode(0) - speex = speex_decoder_init(mode) - speex_decoder_ctl(speex, SPEEX_SET_SAMPLING_RATE, ctypes.byref(ctypes.c_int(8000))) - bits = SpeexBits() - speex_bits_init(ctypes.byref(bits)) - frame_size = ctypes.c_int() - speex_decoder_ctl(speex, SPEEX_GET_FRAME_SIZE, ctypes.byref(frame_size)) - - output = ctypes.create_string_buffer(2000) - offs = 0 - while offs < len(rec): - nbytes = rec[offs] - offs += 1 - if nbytes != 0x26: - continue - buf = ctypes.create_string_buffer(rec[offs:offs + nbytes]) - offs += nbytes - speex_bits_read_from(ctypes.byref(bits), buf, ctypes.c_int(nbytes)) - # this loop looks strange, but its like in roger router and seems to work - for i in range(2): - rc = speex_decode_int(speex, ctypes.byref(bits), output) - if rc == -1: - break - elif rc == -2: - print("Decoding error: corrupted stream?"); - break - if speex_bits_remaining(ctypes.byref(bits)) < 0: - print("Decoding overflow: corrupted stream?"); - break - wav.writeframes(output[0:2 * frame_size.value]) - - wav.close() - speex_bits_destroy(ctypes.byref(bits)) - - - -# Build the url to download the message via smb -# smb://192.168.1.1/fritz.nas/FRITZ/voicebox/rec - -def build_download_url(mid, tam=0): - url = r"//" + env_ip + r"/fritz.nas/FRITZ/voicebox/rec/rec." + str(tam) + r"." + str(mid).zfill(3) - return url - -def download_speex_file(smb_url): - smbclient.register_session(server=env_ip, username=env_user, password=env_pass, auth_protocol="ntlm") - fd = smbclient.open_file(smb_url, mode="rb") - return fd + def download_speex_file(smb_url): + smbclient.register_session(server=env_ip, username=env_user, password=env_pass, auth_protocol="ntlm") + fd = smbclient.open_file(smb_url, mode="rb") + return fd -with urllib.request.urlopen(message_list_url) as f: - doc = f.read() - # Convert the xml formatted message list to dict - messages = xmltodict.parse(doc) + def get_message_list(url): + """ Get and and convert the xml formatted list of messages into a dictionary. """ + with urllib.request.urlopen(url) as f: + doc = f.read() + # Convert the xml formatted message list to dict + messages = xmltodict.parse(doc) + return messages -for a in messages['Root']['Message']: - - msg_info = a['Date'] + " - " + a['Number'] - if len(a['Name']) > 1: - msg_info += " (" + a['Name'] + ") " - msg_tags = {'title': msg_info, 'artist': 'Answerting Machine' ,'album': "TAM" + a['Tam'], 'comment': 'Message of a telephone answering machine'} - - # Select only new messages - message_new = bool(int(a['New'])) - - # Select only messages longer than 5 sec - # No. Unfortunaetly the MessageList excludingly puts 0:01 into Duration tag. - - if message_new == True: - - smb_url = build_download_url(a['Index']) - speex_fd = download_speex_file(smb_url) - speex_convert(speex_fd, "/tmp/message.wav") - # Convert wav to ogg - msg = AudioSegment.from_wav("/tmp/message.wav") + for a in get_message_list(message_list_url)['Root']['Message']: - # Only convert and upload if message is longer than 5 seconds. - if msg.duration_seconds > 5.0: - msg.export("/tmp/message.ogg", format="ogg", tags=msg_tags) + # format the information regarding the message + msg_info = a['Date'] + " - " + a['Number'] + if len(a['Name']) > 1: + msg_info += " (" + a['Name'] + ") " + + # format the string for sound file's meta information + msg_tags = {'title': msg_info, 'artist': 'Answerting Machine' ,'album': "TAM" + a['Tam'], 'comment': 'Message of a telephone answering machine'} + + # Select only new messages + message_new = bool(int(a['New'])) + + if message_new == True: - ### POST MESSAGES TO MATRIX PRIVATE CHAT ### - ############################################ + # Download and convert the speex files to wav + smb_url = build_download_url(a['Index']) + speex_fd = download_speex_file(smb_url) + conv.speex_convert(speex_fd, os.path.join(env_tmp,"message.wav")) + # Convert wav to ogg + msg = AudioSegment.from_wav(os.path.join(env_tmp,"message.wav")) + + # Only if message is longer than 5 seconds ... + if msg.duration_seconds > 5.0: + # ... export to ogg ... + msg.export(os.path.join(env_tmp,"message.ogg"), format="ogg", tags=msg_tags) + + # ... and send message and file to Matrix Room + command = "python3 matrix-commander.py -a " + os.path.join(env_tmp,"message.ogg") + " -m '{}'".format(msg_info) + # os.system(command) - # Formatting message - - # Send message and file to Matrix Room - command = "python3 matrix-commander.py -a /tmp/message.ogg -m '{}'".format(msg_info) - os.system(command) + else: + # Mark MessageInfo as too short for the log + msg_info += " < 6 sec (not posted)" + + # Show that message is new + print("** " + msg_info) + + # Mark processed messages as 'read' + fc.call_action("X_AVM-DE_TAM1", "MarkMessage", NewIndex=0, NewMessageIndex=int(a['Index']), NewMarkedAsRead=1) else: - # Mark MessageInfo as too short - msg_info += " < 6 sec (not posted)" - print("** " + msg_info) - - # Mark processed messages as 'read' - fc.call_action("X_AVM-DE_TAM1", "MarkMessage", NewIndex=0, NewMessageIndex=int(a['Index']), NewMarkedAsRead=1) + # Show that message is already read + print("__ " + msg_info) - - - else: - print("__ " + msg_info) + # ## For testing purposes only +# if a['Date'].endswith('20:53'): +# fc.call_action("X_AVM-DE_TAM1", "MarkMessage", NewIndex=0, NewMessageIndex=int(a['Index']), NewMarkedAsRead=0) + + continue - ## For testing purposes only - if a['Number'].endswith('7714'): - fc.call_action("X_AVM-DE_TAM1", "MarkMessage", NewIndex=0, NewMessageIndex=int(a['Index']), NewMarkedAsRead=0) continue - continue - - +main() +### Monitor the FritzBox and trigger the main script whenever a call disconnects ### +################################################################################### +endedCall(main, env_ip) diff --git a/libs/message/conversion/__init__.py b/libs/message/conversion/__init__.py new file mode 100644 index 0000000..89ff2fa --- /dev/null +++ b/libs/message/conversion/__init__.py @@ -0,0 +1,85 @@ + +import wave, ctypes + + +### Convert fb speex format to wav ### +###################################### +""" +This piece of code is just copied & pasted from https://git.savannah.nongnu.org/cgit/fbvbconv-py.git/tree/fbvbconv.py +which has kindly managed to convert the messages of the answering machine of fritzboxes from a specially configured Speex format to a wave file. All appreciation to the author(s). +""" + +speexlib = ctypes.cdll.LoadLibrary("libspeex.so.1") +SPEEX_SET_SAMPLING_RATE = 24 +SPEEX_GET_FRAME_SIZE = 3 + +class SpeexMode(ctypes.c_void_p): + pass +class Speex(ctypes.c_void_p): + pass +class SpeexBits(ctypes.Structure): + _fields_ = [ + ('chars', ctypes.c_char_p), + ('nbBits', ctypes.c_int), + ('charPtr', ctypes.c_int), + ('bitPtr', ctypes.c_int), + ('owner', ctypes.c_int), + ('overflow', ctypes.c_int), + ('but_size', ctypes.c_int), + ('reserved1', ctypes.c_int), + ('reserved2', ctypes.c_void_p) + ] + +speex_lib_get_mode = speexlib.speex_lib_get_mode +speex_lib_get_mode.restype = SpeexMode +speex_decoder_init = speexlib.speex_decoder_init +speex_decoder_init.restype = Speex +speex_decoder_ctl = speexlib.speex_decoder_ctl +speex_bits_init = speexlib.speex_bits_init +speex_bits_read_from = speexlib.speex_bits_read_from +speex_decode_int = speexlib.speex_decode_int +speex_bits_remaining = speexlib.speex_bits_remaining +speex_bits_destroy = speexlib.speex_bits_destroy + + +def speex_convert(inp, outp): +# rec = open(inp, 'rb').read() + rec = inp.read() + wav = wave.open(outp, 'wb') + wav.setnchannels(1) + wav.setsampwidth(2) + wav.setframerate(8000) + + mode = speex_lib_get_mode(0) + speex = speex_decoder_init(mode) + speex_decoder_ctl(speex, SPEEX_SET_SAMPLING_RATE, ctypes.byref(ctypes.c_int(8000))) + bits = SpeexBits() + speex_bits_init(ctypes.byref(bits)) + frame_size = ctypes.c_int() + speex_decoder_ctl(speex, SPEEX_GET_FRAME_SIZE, ctypes.byref(frame_size)) + + output = ctypes.create_string_buffer(2000) + offs = 0 + while offs < len(rec): + nbytes = rec[offs] + offs += 1 + if nbytes != 0x26: + continue + buf = ctypes.create_string_buffer(rec[offs:offs + nbytes]) + offs += nbytes + speex_bits_read_from(ctypes.byref(bits), buf, ctypes.c_int(nbytes)) + # this loop looks strange, but its like in roger router and seems to work + for i in range(2): + rc = speex_decode_int(speex, ctypes.byref(bits), output) + if rc == -1: + break + elif rc == -2: + print("Decoding error: corrupted stream?"); + break + if speex_bits_remaining(ctypes.byref(bits)) < 0: + print("Decoding overflow: corrupted stream?"); + break + wav.writeframes(output[0:2 * frame_size.value]) + + wav.close() + speex_bits_destroy(ctypes.byref(bits)) diff --git a/libs/monitoring/__init__.py b/libs/monitoring/__init__.py new file mode 100644 index 0000000..c137fc2 --- /dev/null +++ b/libs/monitoring/__init__.py @@ -0,0 +1,36 @@ +import queue +from fritzconnection.core.fritzmonitor import FritzMonitor + +### Monitor the calls of a fritzbox continously ### +################################################### + +def watch_disconnect(monitor, event_queue, func, healthcheck_interval=10): + while True: + try: + event = event_queue.get(timeout=healthcheck_interval) + except queue.Empty: + # check health: + if not monitor.is_alive: + raise OSError("Error: fritzmonitor connection failed") + else: + # do event processing here: + print(event) + if 'DISCONNECT' in event: + print("Anruf beendet. Jetzt den AB checken.\n") + func() + + + +def endedCall(func, fritz_ip='192.168.1.1'): + """ + Call this to trigger a given function if a call is disconnected + """ + try: + # as a context manager FritzMonitor will shut down the monitor thread + with FritzMonitor(address=fritz_ip) as monitor: + event_queue = monitor.start() + watch_disconnect(monitor, event_queue, func) + except (OSError, KeyboardInterrupt) as err: + print(err) + +