"""Parser for iOS Discord message (JSON) files."""
import codecs
import json
import os
from dfdatetime import time_elements as dfdatetime_time_elements
from plaso.containers import events
from plaso.lib import errors
from plaso.parsers import interface
from plaso.parsers import manager
[docs]
class IOSDiscordMessageEventData(events.EventData):
"""iOS discord message event data.
Attributes:
attachment_name (str): The attachment filename.
attachment_proxy_urls (str): The attachment proxy URL.
attachment_size (int): The attachment size.
attachment_type (str): The attachment type.
channel_identifier (str): identifier of the user channel.
content (str): Message content.
edited_timestamp (str): Message edit time.
sent_time (dfdatetime.DateTimeValues): Message timestamp.
user_identifier (str): ID of the message author.
username (str): The username of the message sender.
"""
DATA_TYPE = 'ios:discord:message'
[docs]
def __init__(self):
"""Initializes event data."""
super().__init__(data_type=self.DATA_TYPE)
self.attachment_name = None
self.attachment_proxy_url = None
self.attachment_size = None
self.attachment_type = None
self.channel_identifier = None
self.content = None
self.sent_time = None
self.user_identifier = None
self.username = None
[docs]
class IOSDiscordParser(interface.FileObjectParser):
"""Parses iOS discord message files."""
NAME = 'discord_ios'
DATA_FORMAT = 'iOS discord message'
REQUIRED_MESSAGE_KEYS = frozenset([
'attachments', 'author', 'channel_id', 'content', 'timestamp'])
_ENCODING = 'utf-8'
_MAXIMUM_FILE_SIZE = 16 * 1024 * 1024
def _GetDateTimeValue(self, parser_mediator, timestamp):
"""Retrieves a date and time value.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfVFS.
timestamp (Optional[str]): timestamp value.
Returns:
dfdatetime.TimeElementsInMicroseconds: date and time value or None if
not available.
"""
if timestamp is None:
return None
try:
date_time = dfdatetime_time_elements.TimeElementsInMicroseconds()
date_time.CopyFromStringISO8601(timestamp)
except ValueError as exception:
parser_mediator.ProduceExtractionWarning(
f'Unable to parse timestamp value: {timestamp:s} with error: '
f'{exception!s}')
date_time = None
return date_time
[docs]
def ParseFileObject(self, parser_mediator, file_object):
"""Parses a iOS discord message file."""
# First check for initial 2 characters being open brace and open list.
if file_object.read(2) != b'[{':
display_name = parser_mediator.GetDisplayName()
raise errors.WrongParser(
f'[{self.NAME!s}] {display_name!s} is not a valid Discord messages '
f'file, missing opening brace and list')
file_object.seek(0, os.SEEK_SET)
# Note that _MAXIMUM_FILE_SIZE prevents this read to become too large.
file_content = file_object.read()
try:
file_content = codecs.decode(file_content, self._ENCODING)
except (UnicodeDecodeError, json.JSONDecodeError):
display_name = parser_mediator.GetDisplayName()
raise errors.WrongParser(
f'[{self.NAME!s}] {display_name!s} is not a valid Discord messages '
f'file, unable to decode as UTF-8')
# Second verify it is valid JSON.
try:
json_dict = json.loads(file_content)
except OSError as exception:
display_name = parser_mediator.GetDisplayName()
raise errors.WrongParser(
f'[{self.NAME!s}] Unable to open file {display_name!s} with error: '
f'{exception!s}')
except ValueError as exception:
display_name = parser_mediator.GetDisplayName()
raise errors.WrongParser(
f'[{self.NAME!s}] {display_name!s} is not a valid Discord messages '
f'file, unable to decode as JSON with error: {exception!s}')
# Third verify the file has the correct keys for a Discord messages file.
messages = json_dict or [{}]
if not set(self.REQUIRED_MESSAGE_KEYS).issubset(set(messages[0].keys())):
raise errors.WrongParser('File does not contain Discord messages data')
for message in messages:
attachments = message.get('attachments') or [{}]
timestamp = message.get('timestamp')
event_data = IOSDiscordMessageEventData()
event_data.attachment_name = attachments[0].get('filename') or None
event_data.attachment_proxy_url = attachments[0].get('proxy_url') or None
event_data.attachment_size = attachments[0].get('size') or None
event_data.attachment_type = attachments[0].get('content_type') or None
event_data.channel_identifier = message.get('channel_id') or None
event_data.content = message.get('content') or None
event_data.sent_time = self._GetDateTimeValue(parser_mediator, timestamp)
event_data.user_identifier = message.get('author', {}).get('id') or None
event_data.username = message.get('author', {}).get('username') or None
parser_mediator.ProduceEventData(event_data)
manager.ParsersManager.RegisterParser(IOSDiscordParser)