Merge pull request #193 from m1ndy/feature/export-reactions

feat: Add support for exporting message reactions
This commit is contained in:
Knugi
2026-01-19 20:53:18 +08:00
committed by GitHub
3 changed files with 92 additions and 2 deletions

View File

@@ -92,6 +92,7 @@ def messages(db, data, media_folder, timezone_offset, filter_date, filter_chat,
_process_single_message(data, content, table_message, timezone_offset)
pbar.update(1)
total_time = pbar.format_dict['elapsed']
_get_reactions(db, data)
logger.info(f"Processed {total_row_number} messages in {convert_time_unit(total_time)}{CLEAR_LINE}")
# Helper functions for message processing
@@ -495,6 +496,76 @@ def _format_message_text(text):
return text
def _get_reactions(db, data):
"""
Process message reactions. Only new schema is supported.
Chat filter is not applied here at the moment. Maybe in the future.
"""
c = db.cursor()
try:
# Check if tables exist, old schema might not have reactions or in somewhere else
c.execute("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='message_add_on'")
if c.fetchone()[0] == 0:
return
logger.info("Processing reactions...\r")
c.execute("""
SELECT
message_add_on.parent_message_row_id,
message_add_on_reaction.reaction,
message_add_on.from_me,
jid.raw_string as sender_jid_raw,
chat_jid.raw_string as chat_jid_raw
FROM message_add_on
INNER JOIN message_add_on_reaction
ON message_add_on._id = message_add_on_reaction.message_add_on_row_id
LEFT JOIN jid
ON message_add_on.sender_jid_row_id = jid._id
LEFT JOIN chat
ON message_add_on.chat_row_id = chat._id
LEFT JOIN jid chat_jid
ON chat.jid_row_id = chat_jid._id
""")
except sqlite3.OperationalError:
logger.warning(f"Could not fetch reactions (schema might be too old or incompatible){CLEAR_LINE}")
return
rows = c.fetchall()
total_row_number = len(rows)
with tqdm(total=total_row_number, desc="Processing reactions", unit="reaction", leave=False) as pbar:
for row in rows:
parent_id = row["parent_message_row_id"]
reaction = row["reaction"]
chat_id = row["chat_jid_raw"]
if chat_id and chat_id in data:
chat = data[chat_id]
if parent_id in chat._messages:
message = chat._messages[parent_id]
# Determine sender name
sender_name = None
if row["from_me"]:
sender_name = "You"
elif row["sender_jid_raw"]:
sender_jid = row["sender_jid_raw"]
if sender_jid in data:
sender_name = data[sender_jid].name
if not sender_name:
sender_name = sender_jid.split('@')[0] if "@" in sender_jid else sender_jid
if not sender_name:
sender_name = "Unknown"
message.reactions[sender_name] = reaction
pbar.update(1)
total_time = pbar.format_dict['elapsed']
logger.info(f"Processed {total_row_number} reactions in {convert_time_unit(total_time)}{CLEAR_LINE}")
def media(db, data, media_folder, filter_date, filter_chat, filter_empty, separate_media=True, fix_dot_files=False):
"""
Process WhatsApp media files from the database.

View File

@@ -337,6 +337,7 @@ class Message:
self.caption = None
self.thumb = None # Android specific
self.sticker = False
self.reactions = {}
def to_json(self) -> Dict[str, Any]:
"""Convert message to JSON-serializable dict."""

View File

@@ -230,7 +230,7 @@
</div>
</div>
</div>
<div class="bg-whatsapp-light rounded-lg p-2 max-w-[80%] shadow-sm">
<div class="bg-whatsapp-light rounded-lg p-2 max-w-[80%] shadow-sm relative {% if msg.reactions %}mb-2{% endif %}">
{% if msg.reply is not none %}
<a href="#{{msg.reply}}" target="_self" class="no-base">
<div class="mb-2 p-1 bg-whatsapp-chat-light rounded border-l-4 border-whatsapp text-sm reply-box">
@@ -289,11 +289,20 @@
{% endif %}
</p>
<p class="text-[10px] text-[#667781] text-right mt-1">{{ msg.time }}</p>
{% if msg.reactions %}
<div class="flex flex-wrap gap-1 mt-1 justify-end absolute -bottom-3 -right-2">
{% for sender, emoji in msg.reactions.items() %}
<div class="bg-white rounded-full px-1.5 py-0.5 text-xs shadow-sm border border-gray-200 cursor-help" title="{{ sender }}">
{{ emoji }}
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% else %}
<div class="flex justify-start items-center group" id="{{ msg.key_id }}">
<div class="bg-white rounded-lg p-2 max-w-[80%] shadow-sm">
<div class="bg-white rounded-lg p-2 max-w-[80%] shadow-sm relative {% if msg.reactions %}mb-2{% endif %}">
{% if msg.reply is not none %}
<a href="#{{msg.reply}}" target="_self" class="no-base">
<div class="mb-2 p-1 bg-whatsapp-chat-light rounded border-l-4 border-whatsapp text-sm reply-box">
@@ -360,6 +369,15 @@
<span class="flex-grow min-w-[4px]"></span>
<span class="flex-shrink-0">{{ msg.time }}</span>
</div>
{% if msg.reactions %}
<div class="flex flex-wrap gap-1 mt-1 justify-start absolute -bottom-3 -left-2">
{% for sender, emoji in msg.reactions.items() %}
<div class="bg-gray-100 rounded-full px-1.5 py-0.5 text-xs shadow-sm border border-gray-200 cursor-help" title="{{ sender }}">
{{ emoji }}
</div>
{% endfor %}
</div>
{% endif %}
</div>
<!-- <div class="opacity-0 group-hover:opacity-100 transition-opacity duration-200 relative ml-2">
<div class="relative">