Source code for imessagedb.generate_html

from datetime import datetime
import re
import string
import imessagedb
from imessagedb.message import Message
from imessagedb.messages import Messages
from alive_progress import alive_bar

url_pattern = re.compile(r'((https?):((//)|(\\\\))+[\w\d:#@%/;$()~_?+-=\\.&]*)', re.MULTILINE | re.UNICODE)
mailto_pattern = re.compile(r'([\w\-.]+@(\w[\w\-]+\.)+[\w\-]+)', re.MULTILINE | re.UNICODE)





[docs]class HTMLOutput: """ Creates an HTML file (or string) from a Messages list ... There are a number of options in the configuration file that affect how the HTML is created. In the CONTROL section, the following impact the output: copy = True : If the attachments are not copied, they are not available on the HTML output. There are two reasons for that. 1) The web browser does not have access to the directory that the attachments are stored, so it cannot display them 2) Some attachments need to be converted in order to be viewed in the browser, and need a place to live. skip attachments = False : If true, attachments will not be available in the HTML output In the DISPLAY section, the following impact the output: inline attachments = False : If inline attachments are true, then the images will be part of the HTML. If it is false, then there will be a popup when you hover over the attachment text that will show the attachment on demand. This will make the load much faster and the output far less cluttered. me = Me : The name to put for your part of the conversation. It defaults to 'Me'. html background color list = AliceBlue, Cyan, Gold, Lavender, LightGreen, PeachPuff, Wheat html name color list = Blue, DarkCyan, DarkGoldenRod, Purple, DarkGreen, Orange, Sienna : The background and name color of the messages in html output The options for colors can be found here: https://www.w3schools.com/cssref/css_colors.php The way that the color selection works is that it will use the first color on the color list for the first person in the conversation, the second for the second, third for the third, etc. If there are more participants than colors, it will wrap around to the first color. thread background = HoneyDew : The background color of the thread in replies """ def __init__(self, database, me: str, messages: Messages, inline: bool = False, output_file: str = None) -> None: """ Parameters ---------- database : imessagedb.DB An instance of a connected database me : str Your display name messages: imessagedb.DB.Messages The messages inline : bool Display attachments inline or not output_file : str The name of the output file""" self._database = database self._me = me self._messages = messages self._attachment_list = self._database.attachment_list self._inline = inline self._name_map = {} self._color_list = self._get_next_color() self._day = 'UNK' self._html_array = [] self._current_messages_processed = 0 self._current_messages_file = 0 self._file_start_date: datetime = None self._file_end_date: datetime = None self._previous_range: str = None self._last_row_had_conversion = False if output_file is not None: self._output_filename = output_file self._split_output = self._database.config.getint('DISPLAY', 'split output', fallback=0) self._previous_output_filename = None self._current_output_filename = f"{self._output_filename}.html" self._output_file_handle = open(self._current_output_filename, "w") else: self._output_filename = None start_time = self._database.control.get('start time', fallback=None) end_time = self._database.control.get('end time', fallback=None) date_string = "" if start_time: date_string = f" from {start_time}" if end_time: date_string = f"{date_string} until {end_time}" self._file_header_string = f' <div id="file_summary">Exchanged {len(self._messages):,} total messages with ' \ f'{self._messages.title}{date_string}.</div><p>\n' self._html_array.append(self._generate_table(self._messages)) self._print_and_save('</body>\n</html>\n', self._html_array, eof=True)
[docs] def __repr__(self) -> str: return ''.join(self._html_array)
[docs] def save(self, filename: str) -> None: """ Write the output to the output file""" file_handle = open(filename, "w") print('\n'.join(self._html_array), file=file_handle) file_handle.close() return
[docs] def print(self) -> None: """ Print the output to stdout """ print('\n'.join(self._html_array)) return
[docs] def _print_and_save(self, message: str, array: list, new_day: bool = False, eof: bool = False) -> None: """ Save to the output file while it is processing """ if self._current_messages_processed == 0: # If this is a new file, because it is either the first pass through, or if we need to create new file new_file_array = [self._generate_head(), "<body>\n", self._file_header_string, f'{" ":2s}<div class="picboxframe" id="picbox"> <img src="" /> </div>\n', f'{" ":2s}<table style="table-layout: fixed;">\n{" ":4s}<tr>\n'] if self._previous_output_filename: new_file_array.append(f'{" ":6s}<td style="text-align: left; width: 33%;">' f'<a href="file://{self._previous_output_filename}"> &lt </a> </td>\n' f'{" ":6s}<td style="text-align: center; width: 33%;"><div class="next_file">' + f'<a href="file://{self._previous_output_filename}">' + f' Previous Messages {self._previous_range}</a></div></td>\n') else: new_file_array.append(f'{" ":6s}<td style="width: 33%;"> </td>\n' f'{" ":6s}<td style="width: 33%;"> </td>\n') new_file_array.append(f'{" ":6s}<td style="text-align: right; width: 33%;" id="next_page"> </td>\n' f'{" ":4s}</tr>\n{" ":2s}</table>\n\n') new_file_array.append(f'{" ":2s}<table class="main_table">\n{" ":2s}</table>\n') for i in new_file_array: array.append(i) if self._output_filename is not None: print(i, end="", file=self._output_file_handle) array.append(message) self._current_messages_processed += 1 if self._output_filename is None: # We are not writing to a file return if eof: # Normally this gets done on the file split, but the eof indicates its last file, so do it # on the last write description_string = f' This page contains {self._current_messages_processed:,} messages from ' \ f'{self._file_start_date.strftime("%A %Y-%m-%d")} to ' \ f'{self._file_end_date.strftime("%A %Y-%m-%d")}.' print(f' <script>\n' f' el = document.getElementById("file_summary")\n' f' new_text = el.innerHTML.concat("{description_string}")\n' f' el.innerHTML = new_text\n', f' </script>\n', file=self._output_file_handle) print(message, end="", file=self._output_file_handle) if new_day and 0 < self._split_output < self._current_messages_processed: total_processed = self._current_messages_processed self._current_messages_processed = 0 self._current_messages_file += 1 self._previous_output_filename = self._current_output_filename self._current_output_filename = f"{self._output_filename}_{self._current_messages_file:02d}.html" print(f' <p><div class="next_file"><a href="file://{self._current_output_filename}">' f' Next Messages </a></div>\n', end="", file=self._output_file_handle) description_string = f' This page contains {total_processed:,} messages from ' \ f'{self._file_start_date.strftime("%A %Y-%m-%d")} to ' \ f'{self._file_end_date.strftime("%A %Y-%m-%d")}.' self._previous_range = f'<br><div style="font-size: 50%;">' \ f'({self._file_start_date.strftime("%A %Y-%m-%d")} to ' \ f'{self._file_end_date.strftime("%A %Y-%m-%d")})</div>' print(f' <script>\n' f' el = document.getElementById("file_summary")\n' f' new_text = el.innerHTML.concat("{description_string}")\n' f' el.innerHTML = new_text\n' f' document.getElementById("next_page").innerHTML = ' f' "<a href=\'file://{self._current_output_filename}\'> Next Page &gt </a>"\n' f' </script>', file=self._output_file_handle) print('</body>\n</html>\n', end="", file=self._output_file_handle) self._output_file_handle.close() print(f"Creating output file {self._current_output_filename}") self._output_file_handle = open(self._current_output_filename, "w")
[docs] def _generate_thread_row(self, message: Message) -> str: if message.is_from_me: who_data = self._get_name(0) else: who_data = self._get_name(message.handle_id) style = who_data['style'] text = message.text row_string = f'{" ":16s}<tr>\n' \ f'{" ":18s}<td class="reply_name" style="color: {who_data["name_color"]};"> ' \ f'{who_data["name"]}: </td>\n' \ f'{" ":18s}<td class="reply_text_thread">\n' \ f'{" ":20s}<a href="#{message.rowid}">\n' \ f'{" ":22s}<button class="reply_text_{style}" style="background: ' \ f'{who_data["background_color"]};"> ' \ f'{text}</button>\n' \ f'{" ":20s}</a>\n' \ f'{" ":18s}</td>\n' \ f'{" ":16s}</tr>\n' return row_string
[docs] def _generate_thread_table(self, message_list: list, style: str) -> str: table_string = f'{" ":14s}<table class="thread_table_{style}">\n' for message in message_list: table_string = f'{table_string}{self._generate_thread_row(message)}' table_string = f'{table_string}' \ f'{" ":14s}</table>\n' \ f'{" ":14s}<p>\n' return table_string
[docs] def _generate_table(self, message_list: Messages) -> str: table_array = [] self._print_and_save(f'{" ":2s}<table class="main_table">\n', table_array) previous_day = '' message_count = 0 with alive_bar(len(message_list), title="Generating HTML", stats="({rate}, eta: {eta})", comma=True) as bar: for message in message_list: message_count = message_count + 1 today = message.date[:10] if today != previous_day: previous_day = today message_date = datetime(int(message.date[0:4]), int(message.date[5:7]), int(message.date[8:10]), int(message.date[11:13]), int(message.date[14:16]), int(message.date[17:19])) if self._file_start_date is None: self._file_start_date = message_date self._file_end_date = message_date # If it's a new day, end the table, and start a new one self._print_and_save(f'{" ":2s}</table>\n\n', table_array, new_day=True) self._print_and_save(f'{" ":2s}<table class="main_table">\n', table_array) self._day = message_date.strftime('%a') self._last_row_had_conversion = False self._print_and_save(self._generate_row(message), table_array) if self._last_row_had_conversion: bar() else: bar(skipped=True) self._print_and_save(f'{" ":2s}</table>\n', table_array) return ''.join(table_array)
[docs] def _get_next_color(self): """ A generator function to return the next color""" counter = -1 default_background_color_list = 'AliceBlue, Cyan, Gold, Lavender, LightGreen, PeachPuff, Wheat' default_name_color_list = 'Blue, DarkCyan, DarkGoldenRod, Purple, DarkGreen, Orange, Sienna' background_color_list = self._database.config.get('DISPLAY', 'html background color list', fallback=default_background_color_list) name_color_list = self._database.config.get('DISPLAY', 'html name color list', fallback=default_name_color_list) background_colors = background_color_list.translate({ord(c): None for c in string.whitespace}).split(',') name_colors = name_color_list.translate({ord(c): None for c in string.whitespace}).split(',') while True: counter += 1 next_background_color = counter % len(background_colors) next_name_color = counter % len(name_colors) yield [background_colors[next_background_color], name_colors[next_name_color]]
[docs] def _get_name(self, handle_id: str) -> dict: if handle_id not in self._name_map: (background_color, name_color) = next(self._color_list) handle_list = self._database.handles.handles if handle_id == 0: # 0 is me self._name_map[handle_id] = {'name': self._me, 'background_color': background_color, 'name_color': name_color, 'style': 'me'} elif handle_id in handle_list: handle = handle_list[handle_id] self._name_map[handle_id] = {'name': handle.name, 'background_color': background_color, 'name_color': name_color, 'style': 'them'} else: self._name_map[handle_id] = {'name': handle_id, 'background_color': background_color, 'name_color': name_color, 'style': 'them'} return self._name_map[handle_id]
[docs] def _generate_row(self, message: Message) -> str: # Specify if the message is from me, or the other person if message.is_from_me: who_data = self._get_name(0) else: who_data = self._get_name(message.handle_id) who = who_data['name'] style = who_data['style'] # Check to see if we want the media box floating or fixed floating_option = self._database.config['DISPLAY'].get('popup location', fallback='floating') floating = floating_option == 'floating' # If this message is part of a thread, then show the messages in the thread before it thread_table = "" if message.thread_originator_guid: if message.thread_originator_guid in self._messages.guids: original_message = self._messages.guids[message.thread_originator_guid] thread_list = original_message.thread thread_list[original_message.rowid] = original_message print_thread = [] # sort the threads by the date sent for i in sorted(thread_list.values(), key=lambda x: x.date): if i == message: # stop at the current message break print_thread.append(i) thread_table = self._generate_thread_table(print_thread, style) # Generate the attachment string attachments_string = "" if message.attachments: for attachment_key in message.attachments: # If the attachment listed does not exist, then just list is as missing and continue to the next one if attachment_key not in self._attachment_list.attachment_list: attachments_string = f'{attachments_string} <span class="missing"> Attachment missing </span> ' continue attachment = self._attachment_list.attachment_list[attachment_key] # If the attachment exists, but we have it marked to skip, skip it if attachment.skip: continue # If the attachment exists, but is marked as missing, list it as missing and continue if attachment.missing: attachments_string = f'{attachments_string} <span class="missing"> Attachment missing </span> ' continue # If we should copy the attachment, copy it or convert it if attachment.copy: if attachment.conversion_type == 'HEIC': attachment.convert_heic_image(attachment.original_path, attachment.destination_path) self._last_row_had_conversion = True elif attachment.conversion_type == 'Audio' or attachment.conversion_type == 'Video': attachment.convert_audio_video(attachment.original_path, attachment.destination_path) self._last_row_had_conversion = True else: attachment.copy_attachment() self._last_row_had_conversion = True if floating: box_name = f'PopUp{attachment.rowid}' image_box = f'<div class="imageBox" id="PopUp{attachment.rowid}"> <img src="" /> </div>' else: box_name = 'picbox' image_box = '' if attachment.popup_type == 'Picture': if self._inline: attachment_string = f'<p><a href="{attachment.html_path}" target="_blank">' \ f'<img src="{attachment.html_path}" target="_blank"/><p>' \ f' {attachment.html_path} </a>\n' else: attachment_string = f'''<a href="{attachment.html_path}" target="_blank" onMouseOver="ShowPicture('{box_name}',1,'{attachment.html_path}')" onMouseOut="ShowPicture('{box_name}',0)"> {attachment.html_path} </a> ''' elif attachment.popup_type == 'Audio': # Not going to do popups for audio, just inline attachment_string = f'<p><audio controls> <source src="{attachment.html_path}" ' \ f'type="audio/mp3"></audio> <a href="{attachment.html_path}" ' \ f'target="_blank"> {attachment.html_path} </a>\n' elif attachment.popup_type == 'Video': if self._inline: attachment_string = f'<p><video controls> <source src="{attachment.html_path}" ' \ f' type="video/mp4"></video> <p><a href="{attachment.html_path}"' \ f' target="_blank"> {attachment.html_path} </a>\n' else: attachment_string = f'''<a href="{attachment.html_path}" target="_blank" onMouseOver="ShowMovie('{box_name}', 1, '{attachment.html_path}')"> {attachment.html_path} </a> ''' else: attachment_string = f'<a href="{attachment.html_path}" target="_blank"> ' \ f'{attachment.html_path} </a>\n' attachments_string = f'{attachments_string} <p> {attachment_string} {image_box} ' # Structure of the text row. The first three columns are normal, the fourth column is complex # <tr> # <td> date # <td> who # <td> # <table> # <tr hidden> # <td> Edited Row # </tr> # <tr> # <td> Text of message (edited if required) # <td hidden> extra text # <td> info button (if configured) # <tr> text = _replace_url_to_link(f'{message.text} {attachments_string}') # Check for edits on the text. If there are edits, then set up the html to allow for that. The row # doesn't exist if there is no edits edited_string = "" text_cell_edit_row = "" if len(message.edits) > 0: edit_table = f'{" ":14s}<div class="edits_{style}">\n' for i in range(0, len(message.edits)): edit_table = f'{edit_table}{" ":16s}"{message.edits[i]["text"]} <p>\n' edit_table = f'{edit_table}{" ":14s}</div>\n' edited_string = f'<sub><button class="edited_button"' \ f' onclick="ToggleDisplay(\'{message.rowid}editTable\')"> Edited </button></sub>' text_cell_edit_row = f'{" ":10s}<tr id={message.rowid}editTable class="edits">\n' \ f'{" ":12s}<td>\n' \ f'{edit_table} ' \ f'{" ":12s}</td>\n ' \ f'{" ":10s}</tr>\n' # Check for if we want additional data info_text = "" info_button = "" if self._database.control.getboolean('additional details', fallback=False): info_text = f'{" ":12s}<td class="infocell" id={message.rowid}info>\n ' \ f'{" ":14s}<table>\n' \ f'{" ":16s}<tr>\n' \ f'{" ":18s}<td> ChatID: {message.chat_id} </td>\n' \ f'{" ":16s}</tr>\n' \ f'{" ":14s}</table>\n' \ f'{" ":12s}</td>\n' info_button = f'{" ":12s}<td class="button-wrapper"> <button class="text_{style}" ' \ f'onclick="ToggleDisplay(\'{message.rowid}info\')"> ℹ️ </button> </td>\n' # Put together the row cells date_cell = f'{" ":6s}<td class="date"> {self._day} {message.date} </td>\n' name_cell = f'{" ":6s}<td class="name_{style}" style="color: {who_data["name_color"]};"> {who}: </td>\n' text_cell = f'{" ":6s}<td>\n ' \ f'{" ":8s}<table>\n' \ f'{text_cell_edit_row}' \ f'{" ":10s}<tr>\n' \ f'{" ":12s}<td class="text_{style}" style="background: {who_data["background_color"]};">\n' \ f'{thread_table}' \ f'{" ":14s}{text} {edited_string}\n' \ f'{" ":12s}</td>\n' \ f'{info_text}' \ f'{info_button}' \ f'{" ":10s}</tr>\n' \ f'{" ":8s}</table>\n' \ f'{" ":6s}</td>\n' row_string = f'{" ":4s}<tr id={message.rowid}>\n' \ f'{date_cell}' \ f'{name_cell}' \ f'{text_cell}' \ f'{" ":4s}</tr>\n' return row_string
[docs] def _generate_head(self) -> str: popup = self._database.config['DISPLAY'].get('popup location', fallback='upper right') if popup == 'upper right': popup_location = 'right' else: popup_location = 'left' thread_background_color = self._database.config['DISPLAY'].get('thread background', fallback='HoneyDew') css = ''' <style> table {''' + f''' width: 100%; table-layout: auto; ''' + ''' } table.main_table {''' + f''' border-bottom: 3px solid black; border-spacing: 8px; ''' + ''' } table.thread_table_me {''' + f''' width: 50%; margin-right: 0px; margin-left: auto; background: {thread_background_color}; padding: 5px; border-radius: 30px; ''' + ''' } table.thread_table_them {''' + f''' width: 50%; margin-right: auto; margin-left: 0px; background: {thread_background_color}; padding: 5px; border-radius: 30px; ''' + ''' } td {''' + f''' padding: 0px; margin: 0; line-height: 1; ''' + ''' } td.date {''' + f''' text-align: left; width: 150px; vertical-align: text-middle; font-size: 80%; ''' + ''' } td.name_me {''' + f''' text-align: right; font-weight: bold; width: 50px; padding-right: 5px; vertical-align: text-middle; font-size: 80%; ''' + ''' } td.name_them {''' + f''' text-align: right; font-weight: bold; width: 50px; padding-right: 5px; vertical-align: text-middle; font-size: 80%; ''' + ''' } td.text_me {''' + f''' text-align: right; word-wrap: break-word; border-radius: 30px; padding: 15px; border-spacing: 40px; ''' + ''' } td.text_them {''' + f''' text-align: left; word-wrap: break-word; border-radius: 30px; padding: 15px; border-spacing: 40px; ''' + ''' } .edits_me {''' + f''' display: none; font-size: 70%; font-style: italic; text-align: right; border-radius: 30px; ''' + ''' } .edits_them {''' + f''' display: none; font-size: 70%; font-style: italic; text-align: left; border-radius: 30px; ''' + ''' } .infocell {''' + f''' margin-right: 0px; margin-left: auto; width: 20%; text-align: right; font-size: 70%; display: none; ''' + ''' } button.edited_button {''' + f''' font-size: 50%; border-radius: 30px; font-size: 50%; padding-left: 0px; padding-right: 0px; border-spacing: 0px; border-bottom: 0px; margin-right: 0px; margin-left: 0px; text-align: center; border: 0px; color: blue; font-style: italic; background-color: transparent; ''' + ''' } td.button-wrapper {''' + f''' margin-right: 0px; margin-left: 4px; padding-left: 0px; padding-right: 0px; border-spacing: 0px; text-align: center; width:0.1%; background-color: transparent; ''' + ''' } button.text_me {''' + f''' border-radius: 30px; font-size: 50%; padding-left: 0px; padding-right: 0px; border-spacing: 0px; border-bottom: 0px; margin-right: 0px; margin-left: 0px; text-align: center; border: 0px; ''' + ''' } button.text_them {''' + f''' border-radius: 30px; font-size: 50%; padding-left: 0px; padding-right: 0px; border-spacing: 0px; border-bottom: 0px; margin-right: 0px; margin-left: 0px; text-align: center; border: 0px; ''' + ''' } .reply_name { font-size: 50%; text-align: right; } .reply_text_me {''' + f''' border: 2px solid; border-radius: 6px; border-radius: 50px; font-size: 60% ''' + ''' } .reply_text_them {''' + f''' border: 2px solid; border-radius: 6px; border-radius: 50px; font-size: 60% ''' + ''' } td.blank {''' + f''' border: none; width: 50% ''' + ''' } .missing {''' + f''' color: red; ''' + ''' } .badjoin {''' + f''' color: red; ''' + ''' } .imageBox {''' + f''' position: absolute; visibility: hidden; height: 200; border: solid 1px #CCC; padding: 5px; ''' + ''' } img {''' + f''' height: 250px; width: auto; ''' + ''' } .picboxframe {''' + f''' position: fixed; top: 2%; ''' + f'{popup_location}: 2%;' \ ''' background: Blue; transition: all .5s ease; ''' + ''' } .next_file { text-align: center; font-size: 150%; } #file_summary { font-style: italic; } </style>''' script = ''' <script> function ToggleDisplay(id) { if (document.getElementById(id).style.display == "none") { document.getElementById(id).style.display = "inline"; } else { document.getElementById(id).style.display = "none"; } } function ShowPicture(id,show, img) { if (show=="1") { document.getElementById(id).style.visibility = "visible" document.getElementById(id).childNodes[1].src = img; } else if (show=="0") { document.getElementById(id).style.visibility = "hidden" } } function ShowMovie(id, show, movie) { var elem = document.getElementById(id); ''' \ f' var htmlstring = "<video controls onMouseOut=\'ShowMovie(\\\"\" + id + "\\\",0)\'> ' \ f'<source src=\'" + movie + "\'> </video>";' \ ''' if (show == "1") { { elem.style.visibility = "visible"; elem.innerHTML = htmlstring; } } else if (show == "0") { { elem.style.visibility = "hidden"; elem.innerHTML = " " } } } </script>''' head_string = '''<!DOCTYPE html> <html lang="en-US"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> ''' \ f' <title> {self._messages.title} </title>\n{css}\n{script}\n</head>\n' return head_string