Implement iOS avatar #48

This commit is contained in:
KnugiHK
2023-06-13 19:44:16 +08:00
parent 8cdb694a16
commit e0c2cf5f66
5 changed files with 135 additions and 28 deletions

View File

@@ -161,6 +161,13 @@ def main():
action='store_true', action='store_true',
help="Assume the first message in a chat as sent by me (must be used together with -e)" help="Assume the first message in a chat as sent by me (must be used together with -e)"
) )
parser.add_argument(
"--no-avatar",
dest="no_avatar",
default=False,
action='store_true',
help="Do not render avatar in HTML output"
)
args = parser.parse_args() args = parser.parse_args()
# Check for updates # Check for updates
@@ -261,7 +268,7 @@ def main():
if os.path.isfile(msg_db): if os.path.isfile(msg_db):
with sqlite3.connect(msg_db) as db: with sqlite3.connect(msg_db) as db:
db.row_factory = sqlite3.Row db.row_factory = sqlite3.Row
messages(db, data) messages(db, data, args.media)
media(db, data, args.media) media(db, data, args.media)
vcard(db, data) vcard(db, data)
if not args.no_html: if not args.no_html:
@@ -271,7 +278,8 @@ def main():
args.template, args.template,
args.embedded, args.embedded,
args.offline, args.offline,
args.size args.size,
args.no_avatar
) )
else: else:
print( print(
@@ -283,20 +291,20 @@ def main():
if os.path.isdir(args.media): if os.path.isdir(args.media):
media_path = os.path.join(args.output, args.media) media_path = os.path.join(args.output, args.media)
if os.path.isdir(media_path): if os.path.isdir(media_path):
print("Media directory already exists in output directory. Skipping...") print("\nMedia directory already exists in output directory. Skipping...", end="\n")
else: else:
if not args.move_media: if not args.move_media:
if os.path.isdir(media_path): if os.path.isdir(media_path):
print("WhatsApp directory already exists in output directory. Skipping...") print("\nWhatsApp directory already exists in output directory. Skipping...", end="\n")
else: else:
print("Copying media directory...") print("\nCopying media directory...", end="\n")
shutil.copytree(args.media, media_path) shutil.copytree(args.media, media_path)
else: else:
try: try:
shutil.move(args.media, f"{args.output}/") shutil.move(args.media, f"{args.output}/")
except PermissionError: except PermissionError:
print("Cannot remove original WhatsApp directory. " print("\nCannot remove original WhatsApp directory. "
"Perhaps the directory is opened?") "Perhaps the directory is opened?", end="\n")
else: else:
extract_exported.messages(args.exported, data, args.assume_first_as_me) extract_exported.messages(args.exported, data, args.assume_first_as_me)
if not args.no_html: if not args.no_html:

View File

@@ -1,13 +1,26 @@
import os
from datetime import datetime from datetime import datetime
from typing import Union from typing import Union
from Whatsapp_Chat_Exporter.utility import Device
class ChatStore(): class ChatStore():
def __init__(self, name=None): def __init__(self, type, name=None, media=None):
if name is not None and not isinstance(name, str): if name is not None and not isinstance(name, str):
raise TypeError("Name must be a string or None") raise TypeError("Name must be a string or None")
self.name = name self.name = name
self.messages = {} self.messages = {}
if media is not None:
if type == Device.IOS:
self.my_avatar = os.path.join(media, "Media/Profile/Photo.jpg")
elif type == Device.ANDROID:
self.my_avatar = None # TODO: Add Android support
else:
self.my_avatar = None
else:
self.my_avatar = None
self.their_avatar = None
self.their_avatar_thumb = None
def add_message(self, id, message): def add_message(self, id, message):
if not isinstance(message, Message): if not isinstance(message, Message):

View File

@@ -1,5 +1,6 @@
#!/usr/bin/python3 #!/usr/bin/python3
from glob import glob
import sqlite3 import sqlite3
import json import json
import jinja2 import jinja2
@@ -8,10 +9,10 @@ import shutil
from pathlib import Path from pathlib import Path
from mimetypes import MimeTypes from mimetypes import MimeTypes
from Whatsapp_Chat_Exporter.data_model import ChatStore, Message from Whatsapp_Chat_Exporter.data_model import ChatStore, Message
from Whatsapp_Chat_Exporter.utility import MAX_SIZE, ROW_SIZE, rendering, sanitize_except, determine_day, APPLE_TIME from Whatsapp_Chat_Exporter.utility import MAX_SIZE, ROW_SIZE, rendering, sanitize_except, determine_day, APPLE_TIME, Device
def messages(db, data): def messages(db, data, media_folder):
c = db.cursor() c = db.cursor()
# Get contacts # Get contacts
c.execute("""SELECT count() FROM ZWACHATSESSION""") c.execute("""SELECT count() FROM ZWACHATSESSION""")
@@ -21,7 +22,17 @@ def messages(db, data):
c.execute("""SELECT ZCONTACTJID, ZPARTNERNAME FROM ZWACHATSESSION; """) c.execute("""SELECT ZCONTACTJID, ZPARTNERNAME FROM ZWACHATSESSION; """)
content = c.fetchone() content = c.fetchone()
while content is not None: while content is not None:
data[content["ZCONTACTJID"]] = ChatStore(content["ZPARTNERNAME"]) data[content["ZCONTACTJID"]] = ChatStore(Device.IOS, content["ZPARTNERNAME"], media_folder)
path = f'{media_folder}/Media/Profile/{content["ZCONTACTJID"].split("@")[0]}'
avatars = glob(f"{path}*")
if 0 < len(avatars) <= 1:
data[content["ZCONTACTJID"]].their_avatar = avatars[0]
else:
for avatar in avatars:
if avatar.endswith(".thumb"):
data[content["ZCONTACTJID"]].their_avatar_thumb = avatar
elif avatar.endswith(".jpg"):
data[content["ZCONTACTJID"]].their_avatar = avatar
content = c.fetchone() content = c.fetchone()
# Get message history # Get message history
@@ -49,7 +60,17 @@ def messages(db, data):
_id = content["_id"] _id = content["_id"]
Z_PK = content["Z_PK"] Z_PK = content["Z_PK"]
if _id not in data: if _id not in data:
data[_id] = ChatStore() data[_id] = ChatStore(Device.IOS)
path = f'{media_folder}/Media/Profile/{_id.split("@")[0]}'
avatars = glob(f"{path}*")
if 0 < len(avatars) <= 1:
data[_id].their_avatar = avatars[0]
else:
for avatar in avatars:
if avatar.endswith(".thumb"):
data[_id].their_avatar_thumb = avatar
elif avatar.endswith(".jpg"):
data[_id].their_avatar = avatar
ts = APPLE_TIME + content["ZMESSAGEDATE"] ts = APPLE_TIME + content["ZMESSAGEDATE"]
message = Message( message = Message(
from_me=content["ZISFROMME"], from_me=content["ZISFROMME"],
@@ -232,7 +253,8 @@ def create_html(
template=None, template=None,
embedded=False, embedded=False,
offline_static=False, offline_static=False,
maximum_size=None maximum_size=None,
no_avatar=False
): ):
if template is None: if template is None:
template_dir = os.path.dirname(__file__) template_dir = os.path.dirname(__file__)
@@ -243,11 +265,12 @@ def create_html(
templateLoader = jinja2.FileSystemLoader(searchpath=template_dir) templateLoader = jinja2.FileSystemLoader(searchpath=template_dir)
templateEnv = jinja2.Environment(loader=templateLoader) templateEnv = jinja2.Environment(loader=templateLoader)
templateEnv.globals.update(determine_day=determine_day) templateEnv.globals.update(determine_day=determine_day)
templateEnv.globals.update(no_avatar=no_avatar)
templateEnv.filters['sanitize_except'] = sanitize_except templateEnv.filters['sanitize_except'] = sanitize_except
template = templateEnv.get_template(template_file) template = templateEnv.get_template(template_file)
total_row_number = len(data) total_row_number = len(data)
print(f"\nCreating HTML...(0/{total_row_number})", end="\r") print(f"\nGenerating chats...(0/{total_row_number})", end="\r")
if not os.path.isdir(output_folder): if not os.path.isdir(output_folder):
os.mkdir(output_folder) os.mkdir(output_folder)
@@ -305,7 +328,10 @@ def create_html(
render_box, render_box,
contact, contact,
w3css, w3css,
f"{safe_file_name}-{current_page + 1}.html" f"{safe_file_name}-{current_page + 1}.html",
chat.my_avatar,
chat.their_avatar,
chat.their_avatar_thumb
) )
render_box = [message] render_box = [message]
current_size = 0 current_size = 0
@@ -323,17 +349,31 @@ def create_html(
render_box, render_box,
contact, contact,
w3css, w3css,
False False,
chat.my_avatar,
chat.their_avatar,
chat.their_avatar_thumb
) )
else: else:
render_box.append(message) render_box.append(message)
else: else:
output_file_name = f"{output_folder}/{safe_file_name}.html" output_file_name = f"{output_folder}/{safe_file_name}.html"
rendering(output_file_name, template, name, chat.get_messages(), contact, w3css, False) rendering(
output_file_name,
template,
name,
chat.get_messages(),
contact,
w3css,
False,
chat.my_avatar,
chat.their_avatar,
chat.their_avatar_thumb
)
if current % 10 == 0: if current % 10 == 0:
print(f"Creating HTML...({current}/{total_row_number})", end="\r") print(f"Generating chats...({current}/{total_row_number})", end="\r")
print(f"Creating HTML...({total_row_number}/{total_row_number})", end="\r") print(f"Generating chats...({total_row_number}/{total_row_number})", end="\r")
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -53,20 +53,40 @@ def check_update():
return 0 return 0
def rendering(output_file_name, template, name, msgs, contact, w3css, next): def rendering(
output_file_name,
template,
name,
msgs,
contact,
w3css,
next,
my_avatar,
their_avatar,
their_avatar_thumb
):
if their_avatar_thumb is None and their_avatar is not None:
their_avatar_thumb = their_avatar
with open(output_file_name, "w", encoding="utf-8") as f: with open(output_file_name, "w", encoding="utf-8") as f:
f.write( f.write(
template.render( template.render(
name=name, name=name,
msgs=msgs, msgs=msgs,
my_avatar=None, my_avatar=my_avatar,
their_avatar=f"WhatsApp/Avatars/{contact}.j", their_avatar=their_avatar,
their_avatar_thumb=their_avatar_thumb,
w3css=w3css, w3css=w3css,
next=next next=next
) )
) )
class Device(Enum):
IOS = "ios"
ANDROID = "android"
EXPORTED = "exported"
# Android Specific # Android Specific
CRYPT14_OFFSETS = ( CRYPT14_OFFSETS = (
{"iv": 67, "db": 191}, {"iv": 67, "db": 191},

View File

@@ -58,6 +58,12 @@
border-color: rgba(0,0,0,0); border-color: rgba(0,0,0,0);
} }
} }
.avatar {
border-radius:50%;
overflow:hidden;
max-width: 64px;
max-height: 64px;
}
</style> </style>
</head> </head>
<body> <body>
@@ -77,7 +83,11 @@
<div style="padding-left: 10px; text-align: right; color: #3892da;">You</div> <div style="padding-left: 10px; text-align: right; color: #3892da;">You</div>
</div> </div>
<div class="w3-row"> <div class="w3-row">
{% if not no_avatar and my_avatar is not none %}
<div class="w3-col m10 l10"> <div class="w3-col m10 l10">
{% else %}
<div class="w3-col m12 l12">
{% endif %}
<div style="text-align: right;"> <div style="text-align: right;">
{% if msg.reply is not none %} {% if msg.reply is not none %}
<div class="reply"> <div class="reply">
@@ -92,7 +102,7 @@
</div> </div>
{% endif %} {% endif %}
{% if msg.meta == true or msg.media == false and msg.data is none %} {% if msg.meta == true or msg.media == false and msg.data is none %}
<div style="text-align: center;" class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar"> <div style="text-align: center;" class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar w3-threequarter w3-center">
<p>{{ msg.data or 'Not supported WhatsApp internal message' }}</p> <p>{{ msg.data or 'Not supported WhatsApp internal message' }}</p>
</div> </div>
{% else %} {% else %}
@@ -110,7 +120,7 @@
<source src="{{ msg.data }}" /> <source src="{{ msg.data }}" />
</video> </video>
{% elif "/" in msg.mime %} {% elif "/" in msg.mime %}
<div style="text-align: center;" class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar"> <div style="text-align: center;" class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar w3-threequarter w3-center">
<p>The file cannot be displayed here, however it should be located at <a href="./{{ msg.data }}">here</a></p> <p>The file cannot be displayed here, however it should be located at <a href="./{{ msg.data }}">here</a></p>
</div> </div>
{% else %} {% else %}
@@ -124,7 +134,13 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="w3-col m2 l2" style="padding-left: 10px"><img src="{{ my_avatar }}" onerror="this.style.display='none'"></div> {% if not no_avatar and my_avatar is not none %}
<div class="w3-col m2 l2" style="padding-left: 10px">
<a href="{{ my_avatar }}">
<img src="{{ my_avatar }}" onerror="this.style.display='none'" class="avatar">
</a>
</div>
{% endif %}
</div> </div>
{% else %} {% else %}
<div class="w3-row"> <div class="w3-row">
@@ -138,8 +154,18 @@
<div style="text-align: right; color:#70777c;">{{ msg.time }}</div> <div style="text-align: right; color:#70777c;">{{ msg.time }}</div>
</div> </div>
<div class="w3-row"> <div class="w3-row">
<div class="w3-col m2 l2"><img src="{{ their_avatar }}" onerror="this.style.display='none'"></div> {% if not no_avatar %}
<div class="w3-col m2 l2">
{% if their_avatar is not none %}
<a href="{{ their_avatar }}"><img src="{{ their_avatar_thumb or '' }}" onerror="this.style.display='none'" class="avatar"></a>
{% else %}
<img src="{{ their_avatar_thumb or '' }}" onerror="this.style.display='none'" class="avatar">
{% endif %}
</div>
<div class="w3-col m10 l10"> <div class="w3-col m10 l10">
{% else %}
<div class="w3-col m12 l12">
{% endif %}
<div style="text-align: left;"> <div style="text-align: left;">
{% if msg.reply is not none %} {% if msg.reply is not none %}
<div class="reply"> <div class="reply">
@@ -154,7 +180,7 @@
</div> </div>
{% endif %} {% endif %}
{% if msg.meta == true or msg.media == false and msg.data is none %} {% if msg.meta == true or msg.media == false and msg.data is none %}
<div style="text-align: center;" class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar"> <div style="text-align: center;" class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar w3-threequarter w3-center">
<p>{{ msg.data or 'Not supported WhatsApp internal message' }}</p> <p>{{ msg.data or 'Not supported WhatsApp internal message' }}</p>
</div> </div>
{% else %} {% else %}
@@ -172,7 +198,7 @@
<source src="{{ msg.data }}" /> <source src="{{ msg.data }}" />
</video> </video>
{% elif "/" in msg.mime %} {% elif "/" in msg.mime %}
<div style="text-align: center;" class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar"> <div style="text-align: center;" class="w3-panel w3-border-blue w3-pale-blue w3-rightbar w3-leftbar w3-threequarter w3-center">
<p>The file cannot be displayed here, however it should be located at <a href="./{{ msg.data }}">here</a></p> <p>The file cannot be displayed here, however it should be located at <a href="./{{ msg.data }}">here</a></p>
</div> </div>
{% else %} {% else %}