# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Mobius Forensic Toolkit
# Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023 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 struct
import datetime

import pymobius
from gi.repository import Gtk
from gi.repository import GObject
import mobius

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Code generated by Mobius. Do not edit
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
EXTENSION_ID = 'gigatribe-agent'
EXTENSION_NAME = 'Gigatribe Agent'
EXTENSION_AUTHOR = 'Eduardo Aguiar and Marcia Mendes'
EXTENSION_VERSION = '0.6'
EXTENSION_DESCRIPTION = 'Viewer for Gigatribe files'


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief markup SGML string
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def sgml_text_markup(text):
    return text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief message data holder
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class MessageData(object):

    def __init__(self):
        self.text = ''
        self.timestamp = None
        self.offline = False
        self.sender = None
        self.receivers = []
        self.status_text = None


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief user data holder
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class UserData(object):

    def __init__(self):
        self.user_id = None
        self.user_name = None


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Gigatribe chat decoder
# @reference http://www.scribd.com/doc/57669321/Parsing-the-Chat-Logs-for-GigaTribe-Version-3
# @reference http://doc.qt.digia.com/qt-5.2/datastreamformat.html
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class GigaTribeChatFile(object):

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief initialize object
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __init__(self):
        self.version = None
        self.is_valid = False
        self.__messages = []

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief iterate through object
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __iter__(self):
        return iter(self.__messages)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief decode stream
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def decode(self, fp):

        # signature
        signature = fp.read(2)

        if signature != 'ch':
            return

        # version
        self.is_valid = True
        rawdata = fp.read(fp.size)
        rawsize = len(rawdata)

        # version
        self.version, pos = self.__decode_string(rawdata, 0)

        # messages
        while pos < rawsize:
            message, pos = self.__decode_message(rawdata, pos)
            self.__messages.append(message)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief decode message
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __decode_message(self, data, pos):
        message = MessageData()
        message.id, pos = self.__decode_uint4(data, pos)
        message.offline, pos = self.__decode_bool(data, pos)
        message.timestamp, pos = self.__decode_timestamp(data, pos)

        # sender
        message.sender = UserData()
        message.sender.user_id, pos = self.__decode_uint4(data, pos)
        message.sender.user_name, pos = self.__decode_string(data, pos)

        # receivers
        receiver_ids, pos = self.__decode_uint4(data, pos)

        for i in range(receiver_ids):
            receiver = UserData()
            receiver.user_id, pos = self.__decode_uint4(data, pos)
            message.receivers.append(receiver)

        receiver_names, pos = self.__decode_uint4(data, pos)

        for i in range(receiver_names):
            receiver = message.receivers[i]
            receiver.user_name, pos = self.__decode_string(data, pos)

        # text
        message.text, pos = self.__decode_string(data, pos)
        message.html, pos = self.__decode_string(data, pos)
        message.status_text, pos = self.__decode_string(data, pos)

        # msgsize in bytes
        msgsize, pos = self.__decode_uint8(data, pos)

        return message, pos

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief decode string object
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __decode_string(self, data, pos):
        siz = struct.unpack('>i', data[pos:pos + 4])[0]
        pos += 4

        if siz != -1:
            txt = data[pos:pos + siz].decode('utf-16-be')
            pos += siz
        else:
            txt = ''

        return txt, pos

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief decode bool
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __decode_bool(self, data, pos):
        value = ord(data[pos]) == 1
        return value, pos + 1

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief decode uint 8-bit
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __decode_uint1(self, data, pos):
        value = struct.unpack('>B', data[pos])[0]
        return value, pos + 1

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief decode uint 32-bit
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __decode_uint4(self, data, pos):
        value = struct.unpack('>i', data[pos:pos + 4])[0]
        return value, pos + 4

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief decode uint 64-bit
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __decode_uint8(self, data, pos):
        value = struct.unpack('>q', data[pos:pos + 8])[0]
        return value, pos + 8

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief decode timestamp
    # @reference http://doc.qt.digia.com/qt-5.2/qdatetime.html
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __decode_timestamp(self, data, pos):
        days, pos = self.__decode_uint4(data, pos)
        msec, pos = self.__decode_uint4(data, pos)
        tspec, pos = self.__decode_uint1(data, pos)

        timestamp = datetime.datetime(1, 1, 1, 0, 0, 0) + datetime.timedelta(days=days - 1721426) + datetime.timedelta(
            microseconds=msec * 1000)

        return timestamp, pos


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief extension widget
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class Widget(Gtk.VBox):

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief Build widget
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __init__(self, *args):
        GObject.GObject.__init__(self)
        self.set_border_width(10)
        self.set_spacing(5)
        self.__mediator = pymobius.mediator.copy()
        self.__accel_group = Gtk.AccelGroup()

        # menubar
        menubar = Gtk.MenuBar()
        menubar.show()
        self.pack_start(menubar, False, False, 0)

        item = Gtk.MenuItem.new_with_mnemonic('_File')
        item.show()
        menubar.append(item)

        menu = Gtk.Menu()
        menu.show()
        item.set_submenu(menu)

        item = Gtk.MenuItem.new_with_mnemonic('_Open')
        item.connect("activate", self.__on_file_open)
        item.show()
        menu.append(item)

        item = Gtk.SeparatorMenuItem.new()
        item.show()
        menu.append(item)

        item = Gtk.MenuItem.new_with_mnemonic('_Quit')
        item.connect("activate", self.__on_extension_quit)
        item.show()
        menu.append(item)

        # toolbar
        toolbar = Gtk.Toolbar()
        toolbar.set_style(Gtk.ToolbarStyle.ICONS)
        toolbar.show()
        self.pack_start(toolbar, False, False, 0)

        toolitem = Gtk.ToolButton.new()
        toolitem.set_icon_name('document-open')
        toolitem.connect("clicked", self.__on_file_open)
        toolitem.show()
        toolitem.set_tooltip_text("Open chat file(s)")
        toolbar.insert(toolitem, -1)

        # cookies listview
        self.__chat_tableview = pymobius.mediator.call('ui.new-widget', 'tableview')

        column = self.__chat_tableview.add_column('Timestamp')
        column.is_sortable = True

        column = self.__chat_tableview.add_column('Sender account ID', column_type='int')
        column.is_sortable = True

        column = self.__chat_tableview.add_column('Sender account name')
        column.is_sortable = True

        column = self.__chat_tableview.add_column('Receiver account ID', column_type='int')
        column.is_sortable = True

        column = self.__chat_tableview.add_column('Receiver account name')
        column.is_sortable = True

        column = self.__chat_tableview.add_column('Text')
        column.is_markup = True

        self.__chat_tableview.set_report_id('gigatribe.chat')
        self.__chat_tableview.set_report_name('Gigatribe Chat')
        self.__chat_tableview.set_report_app('%s v%s' % (EXTENSION_NAME, EXTENSION_VERSION))
        self.__chat_tableview.show()

        self.pack_start(self.__chat_tableview.get_ui_widget(), True, True, 0)

        # status bar
        frame = Gtk.Frame()
        frame.set_shadow_type(Gtk.ShadowType.IN)
        frame.show()
        self.pack_end(frame, False, False, 0)

        self.__status_label = Gtk.Label.new()
        self.__status_label.set_selectable(True)
        self.__status_label.set_xalign(0.0)
        self.__status_label.show()
        frame.add(self.__status_label)

        # show info
        self.show()

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief set status label
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __set_status_label(self, text):
        self.__status_label.set_text(text)
        mobius.ui.flush()

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle close button
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_extension_quit(self, widget, *args):
        self.__mediator.call('ui.working-area.close', self.working_area.id)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief show save/ignore/cancel dialog if there are modified items
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def on_widget_stopped(self):

        # show confirmation dialog
        dialog = mobius.ui.message_dialog(mobius.ui.message_dialog.type_question)
        dialog.text = f'Do you want to quit from {EXTENSION_NAME}?'
        dialog.add_button(mobius.ui.message_dialog.button_yes)
        dialog.add_button(mobius.ui.message_dialog.button_no)
        dialog.set_default_response(mobius.ui.message_dialog.button_no)
        rc = dialog.run()

        if rc != mobius.ui.message_dialog.button_yes:
            return True

        # close extension
        self.__mediator.clear()

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # @brief handle file->open
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    def __on_file_open(self, widget, *args):

        # choose file(s)
        fs = Gtk.FileChooserDialog(title='Choose chat files')
        fs.set_select_multiple(True)
        fs.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
        fs.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)

        filefilter = Gtk.FileFilter()
        filefilter.set_name('chat file (*.dat)')
        filefilter.add_pattern('*.dat')
        fs.add_filter(filefilter)

        rc = fs.run()
        uri_list = fs.get_uris()
        fs.destroy()

        if rc != Gtk.ResponseType.OK:
            return

        # retrieve chat files
        self.__chat_tableview.clear()
        message_count = 0

        for uri in uri_list:

            try:
                f = mobius.io.new_file_by_url(uri)
                self.__set_status_label('Reading file %s' % f.name)
                fp = f.new_reader()
                chatfile = GigaTribeChatFile()
                chatfile.decode(fp)

                for message in chatfile:
                    timestamp = str(message.timestamp)[:19]

                    if message.text:
                        text = self.gigatribe_text_markup(message.text)

                    elif message.status_text:
                        text = '<span color="red" font_family="monospace">%s</span>' % sgml_text_markup(
                            message.status_text)

                    else:
                        text = None

                    for receiver in message.receivers:
                        self.__chat_tableview.add_row((timestamp, message.sender.user_id, message.sender.user_name,
                                                       receiver.user_id, receiver.user_name, text))

                    message_count += 1

            except Exception as e:
                mobius.core.logf('WRN ' + str(e))

        # update status label
        if message_count == 0:
            text = 'No message added'

        elif message_count == 1:
            text = '1 message has been added'

        else:
            text = '%d messages have been added' % message_count

        self.__set_status_label(text)

    def gigatribe_text_markup(self, text):
        outstr = ''
        pos = 0
        length = len(text)
        in_span = False

        while pos < length:
            idx = text.find('/', pos)

            if idx == -1:
                outstr += sgml_text_markup(text[pos:])
                pos = length

            else:
                outstr += sgml_text_markup(text[pos:idx])
                pos = idx

                # get escape sequence
                span_attr = []
                while pos < length and text[pos:pos + 1] == '/':
                    if text[pos + 1] == 'c':
                        color = text[pos + 2:pos + 8]
                        span_attr.append('foreground="#%s"' % color)
                        pos += 8
                    elif text[pos + 1] == 'b':
                        span_attr.append('weight="bold"')
                        pos += 2
                    elif text[pos + 1] == 'i':
                        span_attr.append('style="italic"')
                        pos += 2
                    elif text[pos + 1] == 'l':
                        span_attr.append('underline="single"')
                        span_attr.append('foreground="blue"')
                        pos += 2
                    elif text[pos + 1] == '/':
                        outstr += '/'
                        pos += 2
                    else:
                        pos += 1

                # open span, if necessary
                if span_attr:
                    if in_span:
                        outstr += '</span>'
                    outstr += '<span %s>' % (' '.join(span_attr))
                    in_span = True

        if in_span:
            outstr += '</span>'

        return outstr


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief start function
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def pvt_start():
    icon = pymobius.mediator.call('extension.get-icon-path', EXTENSION_ID)
    mobius.core.add_resource('menu.tools.' + EXTENSION_ID, 'Menu Tool: Gigatribe Agent',
                             (icon, EXTENSION_NAME, on_activate))


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief stop function
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def pvt_stop():
    mobius.core.remove_resource('menu.tools.' + EXTENSION_ID)
    pymobius.mediator.call('ui.working-area.del', EXTENSION_ID)


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief on_activate
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def on_activate(item_id):
    working_area = pymobius.mediator.call('ui.working-area.get', EXTENSION_ID)

    if not working_area:
        widget = Widget(pymobius.mediator.copy())
        icon_path = pymobius.mediator.call('extension.get-icon-path', EXTENSION_ID)

        working_area = pymobius.mediator.call('ui.working-area.new', EXTENSION_ID)
        working_area.set_default_size(800, 500)
        working_area.set_title(EXTENSION_NAME)
        working_area.set_icon(icon_path)
        working_area.set_widget(widget)

    working_area.show()
