Skip to content

Commit

Permalink
Implement iOS avatar #48
Browse files Browse the repository at this point in the history
  • Loading branch information
KnugiHK committed Jun 13, 2023
1 parent 8cdb694 commit e0c2cf5
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 28 deletions.
22 changes: 15 additions & 7 deletions Whatsapp_Chat_Exporter/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ def main():
action='store_true',
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()

# Check for updates
Expand Down Expand Up @@ -261,7 +268,7 @@ def main():
if os.path.isfile(msg_db):
with sqlite3.connect(msg_db) as db:
db.row_factory = sqlite3.Row
messages(db, data)
messages(db, data, args.media)
media(db, data, args.media)
vcard(db, data)
if not args.no_html:
Expand All @@ -271,7 +278,8 @@ def main():
args.template,
args.embedded,
args.offline,
args.size
args.size,
args.no_avatar
)
else:
print(
Expand All @@ -283,20 +291,20 @@ def main():
if os.path.isdir(args.media):
media_path = os.path.join(args.output, args.media)
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:
if not args.move_media:
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:
print("Copying media directory...")
print("\nCopying media directory...", end="\n")
shutil.copytree(args.media, media_path)
else:
try:
shutil.move(args.media, f"{args.output}/")
except PermissionError:
print("Cannot remove original WhatsApp directory. "
"Perhaps the directory is opened?")
print("\nCannot remove original WhatsApp directory. "
"Perhaps the directory is opened?", end="\n")
else:
extract_exported.messages(args.exported, data, args.assume_first_as_me)
if not args.no_html:
Expand Down
15 changes: 14 additions & 1 deletion Whatsapp_Chat_Exporter/data_model.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import os
from datetime import datetime
from typing import Union
from Whatsapp_Chat_Exporter.utility import Device


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):
raise TypeError("Name must be a string or None")
self.name = name
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):
if not isinstance(message, Message):
Expand Down
62 changes: 51 additions & 11 deletions Whatsapp_Chat_Exporter/extract_iphone.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/python3

from glob import glob
import sqlite3
import json
import jinja2
Expand All @@ -8,10 +9,10 @@
from pathlib import Path
from mimetypes import MimeTypes
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()
# Get contacts
c.execute("""SELECT count() FROM ZWACHATSESSION""")
Expand All @@ -21,7 +22,17 @@ def messages(db, data):
c.execute("""SELECT ZCONTACTJID, ZPARTNERNAME FROM ZWACHATSESSION; """)
content = c.fetchone()
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()

# Get message history
Expand Down Expand Up @@ -49,7 +60,17 @@ def messages(db, data):
_id = content["_id"]
Z_PK = content["Z_PK"]
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"]
message = Message(
from_me=content["ZISFROMME"],
Expand Down Expand Up @@ -232,7 +253,8 @@ def create_html(
template=None,
embedded=False,
offline_static=False,
maximum_size=None
maximum_size=None,
no_avatar=False
):
if template is None:
template_dir = os.path.dirname(__file__)
Expand All @@ -243,11 +265,12 @@ def create_html(
templateLoader = jinja2.FileSystemLoader(searchpath=template_dir)
templateEnv = jinja2.Environment(loader=templateLoader)
templateEnv.globals.update(determine_day=determine_day)
templateEnv.globals.update(no_avatar=no_avatar)
templateEnv.filters['sanitize_except'] = sanitize_except
template = templateEnv.get_template(template_file)

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):
os.mkdir(output_folder)
Expand Down Expand Up @@ -305,7 +328,10 @@ def create_html(
render_box,
contact,
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]
current_size = 0
Expand All @@ -323,17 +349,31 @@ def create_html(
render_box,
contact,
w3css,
False
False,
chat.my_avatar,
chat.their_avatar,
chat.their_avatar_thumb
)
else:
render_box.append(message)
else:
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:
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__":
Expand Down
26 changes: 23 additions & 3 deletions Whatsapp_Chat_Exporter/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,40 @@ def check_update():
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:
f.write(
template.render(
name=name,
msgs=msgs,
my_avatar=None,
their_avatar=f"WhatsApp/Avatars/{contact}.j",
my_avatar=my_avatar,
their_avatar=their_avatar,
their_avatar_thumb=their_avatar_thumb,
w3css=w3css,
next=next
)
)


class Device(Enum):
IOS = "ios"
ANDROID = "android"
EXPORTED = "exported"


# Android Specific
CRYPT14_OFFSETS = (
{"iv": 67, "db": 191},
Expand Down
38 changes: 32 additions & 6 deletions Whatsapp_Chat_Exporter/whatsapp.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@
border-color: rgba(0,0,0,0);
}
}
.avatar {
border-radius:50%;
overflow:hidden;
max-width: 64px;
max-height: 64px;
}
</style>
</head>
<body>
Expand All @@ -77,7 +83,11 @@
<div style="padding-left: 10px; text-align: right; color: #3892da;">You</div>
</div>
<div class="w3-row">
{% if not no_avatar and my_avatar is not none %}
<div class="w3-col m10 l10">
{% else %}
<div class="w3-col m12 l12">
{% endif %}
<div style="text-align: right;">
{% if msg.reply is not none %}
<div class="reply">
Expand All @@ -92,7 +102,7 @@
</div>
{% endif %}
{% 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>
</div>
{% else %}
Expand All @@ -110,7 +120,7 @@
<source src="{{ msg.data }}" />
</video>
{% 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>
</div>
{% else %}
Expand All @@ -124,7 +134,13 @@
{% endif %}
</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>
{% else %}
<div class="w3-row">
Expand All @@ -138,8 +154,18 @@
<div style="text-align: right; color:#70777c;">{{ msg.time }}</div>
</div>
<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">
{% else %}
<div class="w3-col m12 l12">
{% endif %}
<div style="text-align: left;">
{% if msg.reply is not none %}
<div class="reply">
Expand All @@ -154,7 +180,7 @@
</div>
{% endif %}
{% 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>
</div>
{% else %}
Expand All @@ -172,7 +198,7 @@
<source src="{{ msg.data }}" />
</video>
{% 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>
</div>
{% else %}
Expand Down

0 comments on commit e0c2cf5

Please sign in to comment.