# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Mobius Forensic Toolkit
# Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020 Eduardo Aguiar
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
import os
import os.path
import tempfile
import sqlite3
import shutil
import json
import re
import pymobius
import mobius
import message_parser
import traceback

DEBUG = False

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Message types
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
SYSTEM_MESSAGES = {
  'RichText/Media_AudioMsg' : 'Audio message shared',
  'RichText/UriObject' : 'Picture file shared',
  'RichText/Contacts' : 'Contacts shared',
  'RichText/GenericFile' : 'Generic file shared',
  'PopCard' : 'PopCard received',
}

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Decode %UUUU chars from string
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def pct_decode (s):
  def repl (matchobj):
    return unichr (int (matchobj.group (1), 16))

  s = unicode (s, 'utf-8')
  s = re.sub ('%([0-9a-f]{4})', repl, s)

  return s

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Skype Profile class
# @author Eduardo Aguiar
# @see https://bebinary4n6.blogspot.com/2019/07/
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class Profile (object):

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Initialize object
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __init__ (self, f, item):
    self.__db = None
    self.__item = item
    self.__schema_version = None
    self.__f = f

    # set profile attributes
    self.name = pct_decode (f.name[4:-3])
    self.path = f.path.replace ('/', '\\')

    # set data attributes
    self.__account_loaded = False
    self.__chat_messages_loaded = False
    self.__file_transfers_loaded = False
    self.__conversations_loaded = False
    self.__account = None
    self.__file_transfers = []
    self.__chat_messages = []
    self.__skype_names = {}
    self.__conversation_members = {}

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Get account
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def get_account (self):
    if not self.__account_loaded:
      self.__load_account ()

    return self.__account

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Get chat messages
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def get_chat_messages (self):
    if not self.__chat_messages_loaded:
      self.__load_chat_messages ()

    return self.__chat_messages

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Get file transfers
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def get_file_transfers (self):
    if not self.__file_transfers_loaded:
      self.__load_file_transfers ()

    return self.__file_transfers

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Load account
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __load_account (self):
    db = self.__get_db ()

    cursor = db.execute ('''
       SELECT skypename,
              fullname
         FROM accounts
        WHERE id = 1''')

    row = cursor.fetchone ()

    if row:
      self.__account = pymobius.Data ()
      self.__account.id = row[0]
      self.__account.name = row[1]
      self.__account_loaded = True
      self.__skype_names[self.__account.id] = self.__account.name

    self.__account_loaded = True

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Load file transfers
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __load_file_transfers (self):
    self.__file_transfers_loaded = True

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Load chat messages
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __load_chat_messages (self):
          
    # Load messages
    self.__load_chat_messages_from_s4l_db ()

    # Set skype names from skype account IDs
    for message in self.__chat_messages:

      if not message.sender_name:
        message.sender_name = self.__skype_names.get (message.sender_id)

      recipients = set ()
      for r_id, r_name in message.recipients:
        if not r_name:
          r_name = self.__skype_names.get (r_id)
        recipients.add ((r_id, r_name))
      message.recipients = recipients

    # Set loaded
    self.__chat_messages_loaded = True

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Load chat messages from s4l-<username>.db file
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __load_chat_messages_from_s4l_db (self):
    db = self.__get_db ()

    if not db:
      return

    self.__load_conversations ()

    # process messages
    SQL_STATEMENT = '''
       SELECT nsp_pk,
              nsp_data
         FROM messagesv12'''
  
    for row in db.execute (SQL_STATEMENT):
      try:
        message = self.__load_chat_message (row)
        self.__chat_messages.append (message)

      except Exception, e:
        mobius.core.log ('WRN app.skype (__load_chat_messages_from_s4l_db): ' + str (e) + ' ' + traceback.format_exc ())
      
    db.close ()

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Load chat message from row
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __load_chat_message (self, row):
    if DEBUG:
      print row

    nsp_pk = row[0]
    nsp_data = json.loads (row[1])

    if DEBUG:
      print
      print 'nsp_data'
      for key, value in sorted (nsp_data.iteritems ()):
        print '  ', key, value

    # get members
    conversation_id = nsp_data.get ('conversationId', '')
    members = self.__conversation_members.get (conversation_id, set ([self.name]))
      
    if DEBUG:
      print 'conversation ID:', conversation_id
      print 'members:', members

    # create message
    message = pymobius.Data ()
    message.id = nsp_pk
    message.chatname = nsp_pk# @deprecated
    message.sender_id = None
    message.sender_name = None
    message.recipients = []
    message.compose_time = None
    message.timestamp = None

    # sender ID
    creator = nsp_data.get ('creator', '')
    message.sender_id = creator.split (':', 1)[1]
    
    if DEBUG:
      print 'sender:', message.sender_id

    # recipients
    message.recipients = set ((m_id, self.__skype_names.get (m_id)) for m_id in members if m_id != message.sender_id)
    
    if DEBUG:
      print 'recipients:', message.recipients

    # compose time
    compose_time_data = nsp_data.get ('composeTime')
    if compose_time_data:
      compose_time = mobius.datetime.new_datetime_from_unix_timestamp (int (compose_time_data)//1000)

    # timestamp  
    created_time_data = nsp_data.get ('createdTime')
    if created_time_data:
      message.timestamp = mobius.datetime.new_datetime_from_unix_timestamp (int (created_time_data)//1000)

    # sender name
    smsg = nsp_data.get ('_serverMessages')
    smsg_data = smsg[0]
  
    message.sender_name = smsg_data.get ('imdisplayname')
    if DEBUG:
      print
      print 'SKYPE_NAME', message.sender_id, message.sender_name

      for key, value in sorted (smsg_data.iteritems ()):
        print key, value

    # status/type
    message.status = 0 #None #row[5]
    message.type = nsp_data.get ('messagetype')
    message.conversation_id = conversation_id

    # content
    content = nsp_data.get ('content')
    message.raw_text = (content or u'').rstrip ()

    parser = message_parser.MessageParser (message.raw_text)
    text = SYSTEM_MESSAGES.get (message.type)

    if text:
      text = text.replace ('_', ' ')
      text = text.capitalize ()
      parser.add_element ({u'type' : u'system', u'text' : text})
    
    if message.type == 'PopCard':
      message.text = parser.parse_popcard ()

    else:
      message.text = parser.parse ()

    # add Skype name, if available
    if message.sender_name and message.sender_name != message.sender_id:
      self.__skype_names[message.sender_id] = message.sender_name

    return message

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Load conversations
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __load_conversations (self):
    if self.__conversations_loaded:
      return

    db = self.__get_db ()

    if not db:
      return

    # retrieve conversation members from conversationsv14 table

    SQL_STATEMENT = '''
       SELECT nsp_pk,
              nsp_data
         FROM conversationsv14'''
  
    for row in db.execute (SQL_STATEMENT):
      nsp_pk = row[0]
      nsp_data = json.loads (row[1])

      members = set ()
      members.add (nsp_pk.split (':', 1)[1])
      members.add (self.name)

      conv_data = nsp_data.get ('conv')

      if conv_data:
        for m in conv_data.get ('memberConsumptionHorizonsSorted', []):
          member_id = m.get ('id').split (':', 1)[1]
          members.add (member_id)

      self.__conversation_members[nsp_pk[1:]] = members

    # set conversation members loaded
    self.__conversations_loaded = True

    if DEBUG:
      print 'MEMBERS:', self.__conversation_members

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Retrieve database file
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __get_db (self):

    if not self.__db:
      reader = self.__f.new_reader ()
      if not reader:
        return

      # create temporary .sqlite local file
      ext = os.path.splitext (self.__f.name)[1]
      fd, path = tempfile.mkstemp (suffix=ext)

      fp = open (path, 'w')
      fp.write (reader.read ())
      fp.close ()

      # development only
      if DEBUG:
        shutil.copy (path, '/tmp/skype/%s' % self.__f.name)

      # set handled
      #self.__f.set_handled ()
      self.__db = sqlite3.connect (path)

    return self.__db
