mirror of
https://github.com/KnugiHK/WhatsApp-Chat-Exporter.git
synced 2026-04-27 08:21:33 +00:00
Refactor and add docstrings
This commit is contained in:
@@ -1,37 +1,80 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, tzinfo, timedelta
|
from datetime import datetime, tzinfo, timedelta
|
||||||
from typing import Union, Optional
|
from typing import Union, Optional, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
class Timing():
|
class Timing:
|
||||||
def __init__(self, timezone_offset: Optional[int]):
|
"""
|
||||||
|
Handles timestamp formatting with timezone support.
|
||||||
|
"""
|
||||||
|
def __init__(self, timezone_offset: Optional[int]) -> None:
|
||||||
|
"""
|
||||||
|
Initialize Timing object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timezone_offset (Optional[int]): Hours offset from UTC
|
||||||
|
"""
|
||||||
self.timezone_offset = timezone_offset
|
self.timezone_offset = timezone_offset
|
||||||
|
|
||||||
def format_timestamp(self, timestamp, format):
|
def format_timestamp(self, timestamp: Optional[Union[int, float]], format: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Format a timestamp with the specified format string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timestamp (Optional[Union[int, float]]): Unix timestamp to format
|
||||||
|
format (str): strftime format string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[str]: Formatted timestamp string, or None if timestamp is None
|
||||||
|
"""
|
||||||
if timestamp:
|
if timestamp:
|
||||||
timestamp = timestamp / 1000 if timestamp > 9999999999 else timestamp
|
timestamp = timestamp / 1000 if timestamp > 9999999999 else timestamp
|
||||||
return datetime.fromtimestamp(timestamp, TimeZone(self.timezone_offset)).strftime(format)
|
return datetime.fromtimestamp(timestamp, TimeZone(self.timezone_offset)).strftime(format)
|
||||||
else:
|
return None
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class TimeZone(tzinfo):
|
class TimeZone(tzinfo):
|
||||||
def __init__(self, offset):
|
"""
|
||||||
|
Custom timezone class with fixed offset.
|
||||||
|
"""
|
||||||
|
def __init__(self, offset: int) -> None:
|
||||||
|
"""
|
||||||
|
Initialize TimeZone object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
offset (int): Hours offset from UTC
|
||||||
|
"""
|
||||||
self.offset = offset
|
self.offset = offset
|
||||||
def utcoffset(self, dt):
|
|
||||||
return timedelta(hours=self.offset)
|
def utcoffset(self, dt: Optional[datetime]) -> timedelta:
|
||||||
def dst(self, dt):
|
"""Get UTC offset."""
|
||||||
return timedelta(0)
|
return timedelta(hours=self.offset)
|
||||||
|
|
||||||
|
def dst(self, dt: Optional[datetime]) -> timedelta:
|
||||||
|
"""Get DST offset (always 0)."""
|
||||||
|
return timedelta(0)
|
||||||
|
|
||||||
|
|
||||||
class ChatStore():
|
class ChatStore:
|
||||||
def __init__(self, type, name=None, media=None):
|
"""
|
||||||
|
Stores chat information and messages.
|
||||||
|
"""
|
||||||
|
def __init__(self, type: str, name: Optional[str] = None, media: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Initialize ChatStore object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type (str): Device type (IOS or ANDROID)
|
||||||
|
name (Optional[str]): Chat name
|
||||||
|
media (Optional[str]): Path to media folder
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If name is not a string or 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: Dict[str, 'Message'] = {}
|
||||||
self.type = type
|
self.type = type
|
||||||
if media is not None:
|
if media is not None:
|
||||||
from Whatsapp_Chat_Exporter.utility import Device
|
from Whatsapp_Chat_Exporter.utility import Device
|
||||||
@@ -47,18 +90,20 @@ class ChatStore():
|
|||||||
self.their_avatar_thumb = None
|
self.their_avatar_thumb = None
|
||||||
self.status = None
|
self.status = None
|
||||||
self.media_base = ""
|
self.media_base = ""
|
||||||
|
|
||||||
def add_message(self, id, message):
|
def add_message(self, id: str, message: 'Message') -> None:
|
||||||
|
"""Add a message to the chat store."""
|
||||||
if not isinstance(message, Message):
|
if not isinstance(message, Message):
|
||||||
raise TypeError("message must be a Message object")
|
raise TypeError("message must be a Message object")
|
||||||
self.messages[id] = message
|
self.messages[id] = message
|
||||||
|
|
||||||
def delete_message(self, id):
|
def delete_message(self, id: str) -> None:
|
||||||
|
"""Delete a message from the chat store."""
|
||||||
if id in self.messages:
|
if id in self.messages:
|
||||||
del self.messages[id]
|
del self.messages[id]
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self) -> Dict[str, Any]:
|
||||||
serialized_msgs = {id: msg.to_json() for id, msg in self.messages.items()}
|
"""Convert chat store to JSON-serializable dict."""
|
||||||
return {
|
return {
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'type': self.type,
|
'type': self.type,
|
||||||
@@ -66,17 +111,22 @@ class ChatStore():
|
|||||||
'their_avatar': self.their_avatar,
|
'their_avatar': self.their_avatar,
|
||||||
'their_avatar_thumb': self.their_avatar_thumb,
|
'their_avatar_thumb': self.their_avatar_thumb,
|
||||||
'status': self.status,
|
'status': self.status,
|
||||||
'messages': serialized_msgs
|
'messages': {id: msg.to_json() for id, msg in self.messages.items()}
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_last_message(self):
|
def get_last_message(self) -> 'Message':
|
||||||
|
"""Get the most recent message in the chat."""
|
||||||
return tuple(self.messages.values())[-1]
|
return tuple(self.messages.values())[-1]
|
||||||
|
|
||||||
def get_messages(self):
|
def get_messages(self) -> Dict[str, 'Message']:
|
||||||
|
"""Get all messages in the chat."""
|
||||||
return self.messages.values()
|
return self.messages.values()
|
||||||
|
|
||||||
|
|
||||||
class Message():
|
class Message:
|
||||||
|
"""
|
||||||
|
Represents a single message in a chat.
|
||||||
|
"""
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -87,17 +137,35 @@ class Message():
|
|||||||
received_timestamp: int,
|
received_timestamp: int,
|
||||||
read_timestamp: int,
|
read_timestamp: int,
|
||||||
timezone_offset: int = 0,
|
timezone_offset: int = 0,
|
||||||
message_type: int = None
|
message_type: Optional[int] = None
|
||||||
):
|
) -> None:
|
||||||
|
"""
|
||||||
|
Initialize Message object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
from_me (Union[bool, int]): Whether message was sent by the user
|
||||||
|
timestamp (int): Message timestamp
|
||||||
|
time (Union[int, float, str]): Message time
|
||||||
|
key_id (int): Message unique identifier
|
||||||
|
received_timestamp (int): When message was received
|
||||||
|
read_timestamp (int): When message was read
|
||||||
|
timezone_offset (int, optional): Hours offset from UTC. Defaults to 0
|
||||||
|
message_type (Optional[int], optional): Type of message. Defaults to None
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If time is not a string or number
|
||||||
|
"""
|
||||||
self.from_me = bool(from_me)
|
self.from_me = bool(from_me)
|
||||||
self.timestamp = timestamp / 1000 if timestamp > 9999999999 else timestamp
|
self.timestamp = timestamp / 1000 if timestamp > 9999999999 else timestamp
|
||||||
timing = Timing(timezone_offset)
|
timing = Timing(timezone_offset)
|
||||||
if isinstance(time, int) or isinstance(time, float):
|
|
||||||
|
if isinstance(time, (int, float)):
|
||||||
self.time = timing.format_timestamp(self.timestamp, "%H:%M")
|
self.time = timing.format_timestamp(self.timestamp, "%H:%M")
|
||||||
elif isinstance(time, str):
|
elif isinstance(time, str):
|
||||||
self.time = time
|
self.time = time
|
||||||
else:
|
else:
|
||||||
raise TypeError("Time must be a string or number")
|
raise TypeError("Time must be a string or number")
|
||||||
|
|
||||||
self.media = False
|
self.media = False
|
||||||
self.key_id = key_id
|
self.key_id = key_id
|
||||||
self.meta = False
|
self.meta = False
|
||||||
@@ -107,29 +175,31 @@ class Message():
|
|||||||
self.mime = None
|
self.mime = None
|
||||||
self.message_type = message_type,
|
self.message_type = message_type,
|
||||||
self.received_timestamp = timing.format_timestamp(received_timestamp, "%Y/%m/%d %H:%M")
|
self.received_timestamp = timing.format_timestamp(received_timestamp, "%Y/%m/%d %H:%M")
|
||||||
self.read_timestamp = timing.format_timestamp(read_timestamp, "%Y/%m/%d %H:%M")
|
self.read_timestamp = timing.format_timestamp(read_timestamp, "%Y/%m/%d %H:%M")
|
||||||
# Extra
|
|
||||||
|
# Extra attributes
|
||||||
self.reply = None
|
self.reply = None
|
||||||
self.quoted_data = None
|
self.quoted_data = None
|
||||||
self.caption = None
|
self.caption = None
|
||||||
self.thumb = None # Android specific
|
self.thumb = None # Android specific
|
||||||
self.sticker = False
|
self.sticker = False
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self) -> Dict[str, Any]:
|
||||||
|
"""Convert message to JSON-serializable dict."""
|
||||||
return {
|
return {
|
||||||
'from_me' : self.from_me,
|
'from_me': self.from_me,
|
||||||
'timestamp' : self.timestamp,
|
'timestamp': self.timestamp,
|
||||||
'time' : self.time,
|
'time': self.time,
|
||||||
'media' : self.media,
|
'media': self.media,
|
||||||
'key_id' : self.key_id,
|
'key_id': self.key_id,
|
||||||
'meta' : self.meta,
|
'meta': self.meta,
|
||||||
'data' : self.data,
|
'data': self.data,
|
||||||
'sender' : self.sender,
|
'sender': self.sender,
|
||||||
'safe' : self.safe,
|
'safe': self.safe,
|
||||||
'mime' : self.mime,
|
'mime': self.mime,
|
||||||
'reply' : self.reply,
|
'reply': self.reply,
|
||||||
'quoted_data' : self.quoted_data,
|
'quoted_data': self.quoted_data,
|
||||||
'caption' : self.caption,
|
'caption': self.caption,
|
||||||
'thumb' : self.thumb,
|
'thumb': self.thumb,
|
||||||
'sticker' : self.sticker
|
'sticker': self.sticker
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ from markupsafe import Markup
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from Whatsapp_Chat_Exporter.data_model import ChatStore
|
from Whatsapp_Chat_Exporter.data_model import ChatStore
|
||||||
from typing import Dict, List, Optional, Tuple, Union
|
from typing import Dict, List, Optional, Tuple
|
||||||
try:
|
try:
|
||||||
from enum import StrEnum, IntEnum
|
from enum import StrEnum, IntEnum
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
Reference in New Issue
Block a user