# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Mobius Forensic Toolkit
# Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019 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 pymobius
import pymobius.util
import mobius
import message_parser

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Constants
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
MESSAGE_TYPE_A = {
      4 : 'CONFERENCE_CALL_STARTED',
     10 : 'CHAT_MEMBER_ADDED',
     12 : 'CHAT_MEMBER_REMOVED',
     13 : 'CHAT_ENDED',
     30 : 'CALL_STARTED',
     39 : 'CALL_ENDED',
     50 : 'AUTHORIZATION_REQUESTED',
     51 : 'AUTHORIZATION_GIVEN',
     53 : 'BLOCKED',
     61 : 'TEXT',
     64 : 'SMS_SENT',
     68 : 'FILE_SENT'
}

MESSAGE_TYPE_B = {
      2 : 'TEXT',
      3 : 'PICTURE_SHARED',
      6 : 'FILE_SHARED',
      8 : 'VIDEO_SHARED',
      9 : 'AUDIO_SHARED',
     10 : 'EVENT',
    501 : 'CARD',
   1001 : 'DELETEMEMBER'
}

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Get column names from table
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def get_table_columns (db, table):
  columns = set ()
  SQL_STATEMENT = 'PRAGMA TABLE_INFO (%s)' % table
  
  for row in db.execute (SQL_STATEMENT):
    columns.add (row[1])

  return columns

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Skype Profile class
# @author Eduardo Aguiar
# @see https://arxiv.org/pdf/1603.05369.pdf
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class Profile (object):

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Initialize object
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __init__ (self, folder, item):
    self.__folder = folder
    self.__item = item
    self.__schema_version = None
    self.__db = {}

    # set profile attributes
    self.name = folder.name
    self.path = folder.path.replace ('/', '\\')
    self.folder = folder

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

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @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 Get skype names
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def get_skype_names (self):
    return self.__skype_names

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Load account
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __load_account (self):
    db = self.__get_db ('main.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
    
    # check if table has transfer data columns
    db = self.__get_db ('main.db')
    cols = get_table_columns (db, 'transfers')
    
    if 'status' not in cols:
      return

    # retrieve data
    account = self.get_account ()

    SQL_STATEMENT = '''
       SELECT partner_handle,
              partner_dispname,
              status,
              starttime,
              finishtime,
              filepath,
              filename,
              filesize,
              bytestransferred,
              type
         FROM transfers'''
  
    for row in db.execute (SQL_STATEMENT):
      ft = pymobius.Data ()
      ft.status = row[2]
      ft.start_time = mobius.datetime.new_datetime_from_unix_timestamp (row[3])
      ft.finish_time = mobius.datetime.new_datetime_from_unix_timestamp (row[4])
      ft.path = row[5]
      ft.filename = row[6]
      ft.size = row[7]
      ft.bytes_transferred = row[8]
      ft.type = row[9]
      
      # set from/to accounts
      if ft.type == 1:			# receive
        ft.from_skype_account = row[0]
        ft.from_skype_name = row[1]
        ft.to_skype_account = account.id
        ft.to_skype_name = account.name
        
      elif ft.type == 2:		# send
        ft.from_skype_account = account.id
        ft.from_skype_name = account.name
        ft.to_skype_account = row[0]
        ft.to_skype_name = row[1]

      self.__file_transfers.append (ft)

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Load chat messages
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __load_chat_messages (self):
    self.__load_chat_messages_from_skype_db ()
    self.__load_chat_messages_from_main_db ()
    self.__chat_messages_loaded = True
    
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Load chat messages from skype.db
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __load_chat_messages_from_skype_db (self):
    db = self.__get_db ('skype.db')

    if not db:
      return

    account = self.get_account ()

    SQL_STATEMENT = '''
       SELECT m.id,
              m.originalarrivaltime,
              m.author,
              m.content,
              m.sendingstatus,
              m.messagetype,
              c.id
         FROM messages m,
              conversations c
        WHERE c.dbid = m.convdbid'''
  
    for row in db.execute (SQL_STATEMENT):
      message = pymobius.Data ()
      message.id = row[0]
      message.chatname = row[0]	# @deprecated
      message.timestamp = mobius.datetime.new_datetime_from_unix_timestamp (row[1]//1000)
      message.from_skype_account = row[2].split (':', 1)[1]
      message.from_skype_name = self.__skype_names.get (message.from_skype_account)
      message.status = row[4]
      message.raw_text = (row[3] or '').rstrip ()
      message.text = message_parser.parse (message.raw_text)
      message.type = row[5]
      message.type_str = MESSAGE_TYPE_B.get (message.type)
      
      if message.type not in MESSAGE_TYPE_B:
        mobius.core.log ('app.skype: Unknown MESSAGE_TYPE_B %d. Timestamp: %s' % (message.type, message.timestamp))

      # receiver
      if message.from_skype_account == account.id:
        message.to_skype_account = row[6]
      else:
        message.to_skype_account = account.id

      message.to_skype_name = self.__skype_names.get (message.to_skype_account)

      # add message
      self.__chat_messages.append (message)

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Load chat messages from main.db
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __load_chat_messages_from_main_db (self):
    db = self.__get_db ('main.db')

    if not db:
      return

    cols = get_table_columns (db, 'messages')
    if 'chatname' not in cols:
      return

    SQL_STATEMENT = '''
       SELECT m.chatname,
              m.timestamp,
              m.author,
              m.from_dispname,
              m.body_xml,
              m.chatmsg_status,
              m.chatmsg_type,
              m.type,
              p.identity
         FROM messages m,
              participants p
        WHERE p.convo_id = m.convo_id
          AND p.identity != m.author'''
  
    for row in db.execute (SQL_STATEMENT):
      message = pymobius.Data ()
      message.id = row[0]
      message.chatname = row[0]	# @deprecated
      message.timestamp = mobius.datetime.new_datetime_from_unix_timestamp (row[1])
      message.from_skype_account = row[2]
      message.from_skype_name = row[3]
      message.to_skype_account = row[8]
      message.to_skype_name = self.__skype_names.get (message.to_skype_account)
      message.status = row[5]
      message.chatmsg_type = row[6]
      message.raw_text = (row[4] or '').rstrip ()
      message.text = message_parser.parse (message.raw_text)
      message.type = row[7]
      message.type_str = MESSAGE_TYPE_A.get (message.type)

      system_text = ''

      if message.type == 4:	# CONFERENCE_CALL_STARTED
        system_text = 'Conference call started'

      elif message.type == 10:	# CHAT_MEMBER_ADDED
        system_text = 'Chat member added'

      elif message.type == 12:	# CHAT_MEMBER_REMOVED
        system_text = 'Chat member removed'

      elif message.type == 13:	# CHAT_ENDED
        system_text = 'Chat ended'

      elif message.type == 30:	# CALL STARTED
        message.text.insert (0, ('system', {'text' : 'Call started. '}))

      elif message.type == 39:	# CALL ENDED
        message.text.insert (0, ('system', {'text' : 'Call ended. '}))

      elif message.type == 50:	# AUTHORIZATION REQUESTED
        system_text = 'Authorization requested from %s to add as contact' % message.from_skype_account

      elif message.type == 51:	# AUTHORIZATION_GIVEN
        system_text = 'Authorization given to %s' % message.to_skype_account

      elif message.type not in MESSAGE_TYPE_A:
        mobius.core.log ('app.skype: Unknown MESSAGE_TYPE_A %d. Timestamp: %s' % (message.type, message.timestamp))

      if not message.text and system_text:
        message.text = [('system', {'text' : system_text})]

      self.__chat_messages.append (message)

      # add Skype name, if available
      if message.from_skype_name and message.from_skype_name != message.from_skype_account:
         self.__skype_names[message.from_skype_account] = message.from_skype_name
    
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Retrieve sqlite database
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __get_db (self, db_name):

    if db_name not in self.__db:
      f = self.__folder.get_child_by_name (db_name)
      if not f:
        return

      reader = f.new_reader ()
      if not reader:
        return

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

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

      # development only
      # shutil.copy (path, '/tmp/skype/%s-%s' % (self.name, db_name))
      
      # connect to db
      db = sqlite3.connect (path)
      self.__db[db_name] = db

      # set file handled
      if db_name.lower () != 'main.db':		# preserve main.db for now
        f.set_handled ()

    return self.__db.get (db_name)
