├── MANIFEST.in ├── CHANGELOG.txt ├── .gitignore ├── .travis.yml ├── skype-chatsync-viewer.png ├── pyinstaller ├── make.bat ├── Umut-Pulat-Tulliana-2-Log.ico ├── skype-chatsync-viewer.py └── skype-chatsync-viewer.spec ├── setup.cfg ├── tests ├── __init__.py └── basic_test.py ├── skype_chatsync_reader ├── __init__.py ├── gui.py └── scanner.py ├── LICENSE.txt ├── setup.py ├── README.rst └── gui.wxg /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst CHANGELOG.txt LICENSE.txt -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | Version 0.1 2 | ----------- 3 | 4 | - Initial version. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist 3 | /*.egg 4 | /*.egg-info 5 | /**/__pycache__/ 6 | *.pyc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | script: 5 | - python setup.py develop test 6 | -------------------------------------------------------------------------------- /skype-chatsync-viewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstantint/skype-chatsync-reader/HEAD/skype-chatsync-viewer.png -------------------------------------------------------------------------------- /pyinstaller/make.bat: -------------------------------------------------------------------------------- 1 | rem Create a single-file standalone executable using PyInstaller 2 | 3 | pyinstaller skype-chatsync-viewer.spec -------------------------------------------------------------------------------- /pyinstaller/Umut-Pulat-Tulliana-2-Log.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstantint/skype-chatsync-reader/HEAD/pyinstaller/Umut-Pulat-Tulliana-2-Log.ico -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_build = 3 | tag_svn_revision = false 4 | 5 | [pytest] 6 | addopts = --ignore=setup.py --ignore=build --ignore=dist --doctest-modules 7 | norecursedirs=*.egg 8 | -------------------------------------------------------------------------------- /pyinstaller/skype-chatsync-viewer.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Script file for building an executable bundle via PyInstaller. 3 | 4 | Copyright 2015, Konstantin Tretyakov. 5 | Licensed under MIT. 6 | ''' 7 | 8 | from skype_chatsync_reader.gui import main 9 | 10 | main() -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | skype-chatsync-reader: tests module. 3 | 4 | Meant for use with py.test. 5 | Organize tests into files, each named xxx_test.py 6 | Read more here: http://pytest.org/ 7 | 8 | Copyright 2015, Konstantin Tretyakov 9 | Licensed under MIT 10 | ''' -------------------------------------------------------------------------------- /tests/basic_test.py: -------------------------------------------------------------------------------- 1 | ''' 2 | skype-chatsync-reader: Test module. 3 | 4 | Meant for use with py.test. 5 | Write each test as a function named test_. 6 | Read more here: http://pytest.org/ 7 | 8 | Copyright 2015, Konstantin Tretyakov 9 | Licensed under MIT 10 | ''' 11 | 12 | def test_example(): 13 | assert True 14 | -------------------------------------------------------------------------------- /skype_chatsync_reader/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | skype-chatsync-reader: Main module 3 | 4 | Copyright 2015, Konstantin Tretyakov 5 | Licensed under MIT. 6 | ''' 7 | 8 | 9 | def main(): 10 | ''' 11 | Main function of the boilerplate code is the entry point of the 'skypechatsyncreader' executable script (defined in setup.py). 12 | 13 | Use doctests, those are very helpful. 14 | 15 | >>> main() 16 | Hello 17 | >>> 2 + 2 18 | 4 19 | ''' 20 | 21 | print("Hello") 22 | 23 | -------------------------------------------------------------------------------- /pyinstaller/skype-chatsync-viewer.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | import os 3 | a = Analysis(['skype-chatsync-viewer.py'], 4 | pathex=[os.path.abspath('.')], 5 | hiddenimports=[], 6 | hookspath=None, 7 | runtime_hooks=None) 8 | pyz = PYZ(a.pure) 9 | exe = EXE(pyz, 10 | a.scripts, 11 | a.binaries, 12 | a.zipfiles, 13 | a.datas, 14 | name='skype-chatsync-viewer.exe', 15 | debug=False, 16 | strip=None, 17 | upx=True, 18 | console=False , icon='Umut-Pulat-Tulliana-2-Log.ico') 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Konstantin Tretyakov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ''' 2 | skype-chatsync-reader: Parser and GUI viewer of chatsync/*.dat files from the Skype profile directory 3 | 4 | Note that "python setup.py test" invokes pytest on the package. With appropriately 5 | configured setup.cfg, this will check both xxx_test modules and docstrings. 6 | 7 | Copyright 2015, Konstantin Tretyakov. 8 | Licensed under MIT. 9 | ''' 10 | import sys 11 | from setuptools import setup, find_packages 12 | from setuptools.command.test import test as TestCommand 13 | 14 | # This is a plug-in for setuptools that will invoke py.test 15 | # when you run python setup.py test 16 | class PyTest(TestCommand): 17 | def finalize_options(self): 18 | TestCommand.finalize_options(self) 19 | self.test_args = [] 20 | self.test_suite = True 21 | 22 | def run_tests(self): 23 | import pytest # import here, because outside the required eggs aren't loaded yet 24 | sys.exit(pytest.main(self.test_args)) 25 | 26 | 27 | version = "0.1" 28 | 29 | setup(name="skype-chatsync-reader", 30 | version=version, 31 | description="Parser and GUI viewer of chatsync/*.dat files from the Skype profile directory", 32 | long_description=open("README.rst").read(), 33 | classifiers=[ # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers 34 | 'Development Status :: 7 - Inactive', 35 | 'Programming Language :: Python', 36 | 'Intended Audience :: Information Technology', 37 | 'License :: OSI Approved :: MIT License', 38 | 'Operating System :: OS Independent', 39 | 'Topic :: Communications :: Chat', 40 | 'Topic :: Utilities', 41 | ], 42 | keywords="skype file-format wx log-viewer", # Separate with spaces 43 | author="Konstantin Tretyakov", 44 | author_email="kt@ut.ee", 45 | url="http://github.com/konstantint/skype-chatsync-reader", 46 | license="MIT", 47 | packages=find_packages(exclude=['examples', 'tests']), 48 | include_package_data=True, 49 | zip_safe=True, 50 | tests_require=['pytest'], 51 | cmdclass={'test': PyTest}, 52 | 53 | # TODO: List of packages that this one depends upon: 54 | install_requires=[], 55 | # TODO: List executable scripts, provided by the package (this is just an example) 56 | entry_points={ 57 | 'console_scripts': 58 | ['skype-chatsync-viewer=skype_chatsync_reader.gui:main'] 59 | } 60 | ) 61 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | NB: As of 2017, this tool DOES NOT SUPPORT recent versions of Skype 2 | ------------------------------------------------------------------- 3 | 4 | =============================================================================== 5 | Parser and GUI viewer of chatsync/\*.dat files from the Skype profile directory 6 | =============================================================================== 7 | 8 | Skype stores conversations locally in two places. One is a SQLite database file, for which there are several convenient viewers out there. 9 | Another is a set of ``dat`` files in the ``chatsync`` subdirectory of the profile. The latter contain, among other things, the "removed" messages 10 | along with all the edits. Unfortunately, the format of those dat files does not seem to be documented anywhere, and the readers are scarce. 11 | 12 | The package contains a crude file format parser for the ``dat`` files in the ``chatsync`` directory, created based on the hints, 13 | given by user *kmn* in `this discussion `__. 14 | 15 | As the format specification used is not official and incomplete, the parser is limited in what it can do. 16 | It may fail on some files, and on other files will only be able to extract messages partially. 17 | 18 | In addition, the package contains a simple wx-based GUI tool for searching the log files visually. 19 | 20 | .. image:: http://fouryears.eu/wp-content/uploads/2015/01/skype-chatsync-viewer.png 21 | :align: center 22 | :target: http://fouryears.eu/2015/01/22/skype-removed-messages/ 23 | 24 | Installation 25 | ------------ 26 | 27 | The easiest way to install most Python packages is via ``easy_install`` or ``pip``:: 28 | 29 | $ easy_install skype_chatsync_reader 30 | 31 | If you want to use the GUI tool, you will also need to install `wxPython 2.8 `__ or later (it is not installed automatically). 32 | 33 | A standalone executable version of the GUI tool for Windows can be downloaded `here `__. 34 | 35 | Usage 36 | ----- 37 | 38 | If you want to parse chatsync files programmatically, check out the ``SkypeChatSyncScanner`` and ``SkypeChatSyncParser`` classes in ``skype_chatsync_reader.scanner``. 39 | A typical usage example is:: 40 | 41 | with open(dat_file, 'rb') as f: 42 | s = SkypeChatSyncScanner(f) 43 | s.scan() 44 | p = SkypeChatSyncParser(s) 45 | p.parse() 46 | 47 | Then use ``p.timestamp``, ``p.participants``, and ``p.conversation`` to read out the messages. There convenience function ``parse_chatsync_profile_dir`` will scan 48 | through all the ``dat`` files in the provided ``chatsync`` dir and parse all of them (which can be parsed). 49 | 50 | If you want to use the GUI tool, simply run the script:: 51 | 52 | $ skype-chatsync-viewer 53 | 54 | which is installed into your python's scripts directory together with the package. 55 | 56 | 57 | Issues 58 | ------ 59 | 60 | This is a very crude implementation, written up in a single evening for fun. It is not meant to be production-quality software. There are numerous known and unknown issues. 61 | I do not plan to maintain this actively. Feel free to contribute via `Github `__. 62 | 63 | 64 | Copyright 65 | --------- 66 | 67 | * Copyright 2015, `Konstantin Tretyakov `__ 68 | * MIT License 69 | * The icon used in the single-file executable is (c) `Umut Pulat `__, licensed under LGPL. 70 | -------------------------------------------------------------------------------- /gui.wxg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Skype ChatSync File Viewer 8 | 1 9 | 750, 420 10 | 11 | 12 | 13 | 14 | 15 | mi_open 16 | Specify a chatsync directory to load files from 17 | on_open 18 | 19 | 20 | 21 | mi_find 22 | Search for text in messages 23 | on_find 24 | 25 | 26 | 27 | mi_find_next 28 | Search for the next occurrence of the same string in messages 29 | on_find_next 30 | 31 | 32 | 33 | --- 34 | --- 35 | 36 | 37 | 38 | Exit 39 | Exit 40 | on_quit 41 | 42 | 43 | 44 | 45 | 46 | wxHORIZONTAL 47 | 48 | wxALL|wxEXPAND 49 | 0 50 | 51 | 52 | 53 | wxSPLIT_VERTICAL 54 | 200 55 | pane_right 56 | pane_left 57 | 58 | 59 | 60 | wxHORIZONTAL 61 | 62 | wxALL|wxEXPAND 63 | 0 64 | 65 | 66 | 67 | 68 | on_conversation_selected 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | wxVERTICAL 78 | 79 | wxALL|wxEXPAND 80 | 0 81 | 82 | 83 | 84 | Use Menu → Open to load the chatsync directory. 85 | 86 | 8 87 | modern 88 | 89 | normal 90 | 0 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /skype_chatsync_reader/gui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | # 4 | # generated by wxGlade 0.6.8 (standalone edition) on Thu Jan 22 07:21:12 2015 5 | # 6 | 7 | import wx 8 | import os 9 | import os.path 10 | import sys 11 | from threading import Thread 12 | import traceback 13 | import warnings 14 | from datetime import datetime 15 | from .scanner import parse_chatsync_profile_dir 16 | 17 | # begin wxGlade: dependencies 18 | # end wxGlade 19 | 20 | # begin wxGlade: extracode 21 | # end wxGlade 22 | 23 | 24 | class ChatSyncLoader(Thread): 25 | ''' 26 | A thread object that loads the chatsync conversation objects from a given dirname. 27 | When finished invokes the .on_conversations_loaded on the provided main_frame object. 28 | ''' 29 | 30 | def __init__(self, dirname, main_frame): 31 | Thread.__init__(self) 32 | self.dirname = dirname 33 | self.main_frame = main_frame 34 | self.start() 35 | 36 | def run(self): 37 | try: 38 | conversations = parse_chatsync_profile_dir(self.dirname) 39 | except: 40 | traceback.print_exc() 41 | conversations = [] 42 | wx.CallAfter(self.main_frame.on_conversations_loaded, conversations) 43 | 44 | 45 | class ConversationSearcher(object): 46 | ''' 47 | A utility class for implementing Find/FindNext in a list of conversation objects. 48 | ''' 49 | 50 | def __init__(self, conversations=[]): 51 | self.conversations = conversations 52 | self.current_word = None 53 | 54 | def find(self, word): 55 | self.current_word = word 56 | self.current_conversation_id = 0 57 | self.current_message_id = 0 58 | return self.find_next() 59 | 60 | def find_next(self): 61 | if self.current_word is None or self.current_word == '' or len(self.conversations) == 0: 62 | return None 63 | while True: 64 | if self.current_word in self.conversations[self.current_conversation_id].conversation[self.current_message_id].text: 65 | result = (self.current_conversation_id, self.current_message_id) 66 | self.next_message() 67 | return result 68 | else: 69 | if not self.next_message(): 70 | break 71 | return False 72 | 73 | def next_message(self): 74 | if len(self.conversations) == 0: 75 | return False 76 | self.current_message_id += 1 77 | if self.current_message_id >= len(self.conversations[self.current_conversation_id].conversation): 78 | self.current_conversation_id = (self.current_conversation_id + 1) % len(self.conversations) 79 | self.current_message_id = 0 80 | if self.current_conversation_id == 0: 81 | return False 82 | return True 83 | 84 | 85 | 86 | 87 | class MainFrame(wx.Frame): 88 | ''' 89 | The main and only frame of the application. 90 | ''' 91 | 92 | def __init__(self, *args, **kwds): 93 | # begin wxGlade: MainFrame.__init__ 94 | kwds["style"] = wx.DEFAULT_FRAME_STYLE 95 | wx.Frame.__init__(self, *args, **kwds) 96 | 97 | # Menu Bar 98 | self.menubar = wx.MenuBar() 99 | self.mi_menu = wx.Menu() 100 | self.mi_open = wx.MenuItem(self.mi_menu, wx.ID_ANY, "&Open...\tCtrl+O", "Specify a chatsync directory to load files from", wx.ITEM_NORMAL) 101 | self.mi_menu.AppendItem(self.mi_open) 102 | self.mi_find = wx.MenuItem(self.mi_menu, wx.ID_ANY, "&Find...\tCtrl+F", "Search for text in messages", wx.ITEM_NORMAL) 103 | self.mi_menu.AppendItem(self.mi_find) 104 | self.mi_find_next = wx.MenuItem(self.mi_menu, wx.ID_ANY, "Find &next\tF3", "Search for the next occurrence of the same string in messages", wx.ITEM_NORMAL) 105 | self.mi_menu.AppendItem(self.mi_find_next) 106 | self.mi_menu.AppendSeparator() 107 | self.Exit = wx.MenuItem(self.mi_menu, wx.ID_ANY, "&Exit", "Exit", wx.ITEM_NORMAL) 108 | self.mi_menu.AppendItem(self.Exit) 109 | self.menubar.Append(self.mi_menu, "&Menu") 110 | self.SetMenuBar(self.menubar) 111 | # Menu Bar end 112 | self.splitter = wx.SplitterWindow(self, wx.ID_ANY, style=wx.SP_3D | wx.SP_BORDER) 113 | self.pane_left = wx.Panel(self.splitter, wx.ID_ANY, style=wx.STATIC_BORDER) 114 | self.list_conversations = wx.ListCtrl(self.pane_left, wx.ID_ANY, style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_HRULES | wx.LC_VRULES | wx.SUNKEN_BORDER) 115 | self.pane_right = wx.Panel(self.splitter, wx.ID_ANY, style=wx.NO_BORDER) 116 | self.text_chatcontent = wx.TextCtrl(self.pane_right, wx.ID_ANY, u"Use Menu \u2192 Open to load the chatsync directory.", style=wx.TE_MULTILINE | wx.TE_READONLY) 117 | 118 | self.__set_properties() 119 | self.__do_layout() 120 | 121 | self.Bind(wx.EVT_MENU, self.on_open, self.mi_open) 122 | self.Bind(wx.EVT_MENU, self.on_find, self.mi_find) 123 | self.Bind(wx.EVT_MENU, self.on_find_next, self.mi_find_next) 124 | self.Bind(wx.EVT_MENU, self.on_quit, self.Exit) 125 | self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_conversation_selected, self.list_conversations) 126 | # end wxGlade 127 | 128 | if sys.platform == 'win32': 129 | icon = wx.Icon(sys.executable, wx.BITMAP_TYPE_ICO) 130 | self.SetIcon(icon) 131 | 132 | self.searcher = ConversationSearcher() 133 | self.conversation_message_coords = [] # messageno ->(beginpos, endpos). Used to find begin and end points for selection highlights in the textbox. 134 | self.mi_find.Enable(False) 135 | self.mi_find_next.Enable(False) 136 | 137 | def __set_properties(self): 138 | # begin wxGlade: MainFrame.__set_properties 139 | self.SetTitle("Skype ChatSync File Viewer") 140 | self.SetSize((750, 420)) 141 | self.text_chatcontent.SetFont(wx.Font(8, wx.MODERN, wx.NORMAL, wx.NORMAL, 0, "")) 142 | # end wxGlade 143 | 144 | def __do_layout(self): 145 | # begin wxGlade: MainFrame.__do_layout 146 | sizer_root = wx.BoxSizer(wx.HORIZONTAL) 147 | sizer_right = wx.BoxSizer(wx.VERTICAL) 148 | sizer_left = wx.BoxSizer(wx.HORIZONTAL) 149 | sizer_left.Add(self.list_conversations, 1, wx.ALL | wx.EXPAND, 0) 150 | self.pane_left.SetSizer(sizer_left) 151 | sizer_right.Add(self.text_chatcontent, 1, wx.ALL | wx.EXPAND, 0) 152 | self.pane_right.SetSizer(sizer_right) 153 | self.splitter.SplitVertically(self.pane_left, self.pane_right, 200) 154 | sizer_root.Add(self.splitter, 1, wx.ALL | wx.EXPAND, 0) 155 | self.SetSizer(sizer_root) 156 | self.Layout() 157 | # end wxGlade 158 | 159 | def on_open(self, event): # wxGlade: MainFrame. 160 | default_dir = os.getenv('APPDATA', None) 161 | if default_dir is not None: 162 | default_dir = os.path.join(default_dir, 'Skype') 163 | else: 164 | default_dir = os.path.abspath('~/.Skype') 165 | dialog = wx.DirDialog(self, message='Please, select the "chatsync" directory within a Skype user profile:', defaultPath=default_dir, style=wx.DD_DIR_MUST_EXIST | wx.RESIZE_BORDER) 166 | if (dialog.ShowModal() == wx.ID_OK): 167 | self.list_conversations.ClearAll() 168 | self.text_chatcontent.Clear() 169 | self.text_chatcontent.SetValue("Please wait...") 170 | self.mi_open.Enable(False) 171 | ChatSyncLoader(dialog.GetPath(), self) 172 | 173 | def on_quit(self, event): # wxGlade: MainFrame. 174 | self.Close() 175 | 176 | def on_conversations_loaded(self, conversations): 177 | conversations = [c for c in conversations if not c.is_empty and len(c.conversation) > 0] 178 | conversations.sort(lambda x,y: x.timestamp - y.timestamp) 179 | self.conversations = conversations 180 | self.searcher = ConversationSearcher(conversations) 181 | 182 | if len(conversations) == 0: 183 | self.text_chatcontent.SetValue("Done. No non-empty conversations found or could be loaded.") 184 | self.mi_find.Enable(False) 185 | self.mi_find_next.Enable(False) 186 | else: 187 | self.text_chatcontent.SetValue("Done. Select a conversations using the list on the left to view.") 188 | self.mi_find.Enable() 189 | self.mi_find_next.Enable() 190 | self.list_conversations.ClearAll() 191 | self.list_conversations.InsertColumn(0, "Time") 192 | self.list_conversations.InsertColumn(1, "From") 193 | self.list_conversations.InsertColumn(2, "To") 194 | for c in conversations: 195 | dt = datetime.fromtimestamp(c.timestamp) 196 | self.list_conversations.Append((dt.strftime("%Y-%m-%d %H:%M"), c.participants[0], c.participants[1])) 197 | self.mi_open.Enable() 198 | 199 | def on_conversation_selected(self, event): # wxGlade: MainFrame. 200 | sel_idx = self.list_conversations.GetFirstSelected() 201 | if sel_idx == -1: 202 | return 203 | if sel_idx >= len(self.conversations): 204 | self.text_chatcontent.SetValue("Error... :(") 205 | else: 206 | self.text_chatcontent.SetValue("") 207 | self.conversation_message_coords = [] 208 | if len(self.conversations[sel_idx].conversation) == 0: 209 | self.text_chatcontent.AppendText("[empty]") 210 | total_len = 0 211 | for c in self.conversations[sel_idx].conversation: 212 | dt = datetime.fromtimestamp(c.timestamp).strftime("[%Y-%m-%d %H:%M]") 213 | txt = u"%s [%s]" % (dt, c.author) 214 | if c.is_edit: 215 | txt += u" [edit]" 216 | txt += u": " 217 | txt += c.text + "\n" 218 | txt.replace('\r\n', '\n') 219 | text_len = len(txt) 220 | if len(os.linesep) == 2: 221 | # Note that there is a discrepancy between lineseparators of the control's "Value" and its actual internal state 222 | text_len += txt.count('\n') 223 | self.conversation_message_coords.append((total_len, total_len + text_len - 2)) 224 | total_len += text_len 225 | self.text_chatcontent.AppendText(txt) 226 | 227 | def on_find(self, event): # wxGlade: MainFrame. 228 | dlg = wx.TextEntryDialog(self,message="Enter text to search for:") 229 | if (dlg.ShowModal() == wx.ID_OK and dlg.GetValue() != ''): 230 | result = self.searcher.find(dlg.GetValue()) 231 | if result is None: 232 | wx.MessageBox(self, 'Text not found', 'Search result', wx.ICON_INFORMATION | wx.OK) 233 | else: 234 | self.highlight(result) 235 | 236 | def on_find_next(self, event): # wxGlade: MainFrame. 237 | if self.searcher.current_word is None: 238 | self.on_find(event) 239 | else: 240 | result = self.searcher.find_next() 241 | if result is None: 242 | wx.MessageBox(self, 'Reached end of conversation list', 'Search result', wx.ICON_INFORMATION | wx.OK) 243 | else: 244 | self.highlight(result) 245 | 246 | def highlight(self, coords): 247 | self.list_conversations.Select(coords[0]) 248 | wx.CallAfter(self.highlight_message, coords) 249 | 250 | def highlight_message(self, coords): 251 | if self.list_conversations.GetFirstSelected() == coords[0]: 252 | self.text_chatcontent.SetSelection(*self.conversation_message_coords[coords[1]]) 253 | self.text_chatcontent.SetFocus() 254 | 255 | # end of class MainFrame 256 | 257 | class MyApp(wx.App): 258 | def OnInit(self): 259 | wx.InitAllImageHandlers() 260 | main_frame = MainFrame(None, wx.ID_ANY, "") 261 | self.SetTopWindow(main_frame) 262 | main_frame.Show() 263 | return 1 264 | 265 | # end of class MyApp 266 | 267 | def main(): 268 | app = MyApp(0) 269 | app.MainLoop() 270 | 271 | if __name__ == "__main__": 272 | main() -------------------------------------------------------------------------------- /skype_chatsync_reader/scanner.py: -------------------------------------------------------------------------------- 1 | ''' 2 | A file format parser for Skype's "chatsync" files. 3 | Format as described by kmn in http://www.hackerfactor.com/blog/index.php?/archives/231-Skype-Logs.html#c1066 4 | 5 | As the format specification used is not official and incomplete, the parser is limited in what it can do. 6 | It may fail on some files, and on other files will only be able to extract messages partially. 7 | 8 | Copyright 2015, Konstantin Tretyakov. 9 | MIT License. 10 | ''' 11 | 12 | from struct import unpack, calcsize 13 | from collections import namedtuple 14 | from datetime import datetime 15 | import warnings 16 | import os.path 17 | from glob import glob 18 | 19 | 20 | class ScanException(Exception): 21 | def __init__(self, message): 22 | super(ScanException, self).__init__(message) 23 | 24 | 25 | class FileHeader(namedtuple('FileHeader', 'signature timestamp data_size padding')): 26 | __format__ = '<5sII19s' 27 | def validate(self, scanner): 28 | if self.signature != 'sCdB\x07': 29 | raise ScanException("Error scanning header in %s. Invalid signature: %s." % (scanner.name, self.signature)) 30 | if self.padding != '\x00'*19: 31 | warnings.warn("Header padding not all zeroes in %s." % scanner.name) 32 | scanner.warnings += 1 33 | 34 | Block = namedtuple ('Block', 'block_header block_data') 35 | 36 | class BlockHeader(namedtuple('BlockHeader', 'data_size x type padding')): 37 | __format__ = ' 6: 43 | raise ScanException("Error scanning block #%d in %s. Type field value %d invalid." % (len(scanner.blocks) + 1, scanner.name, self.type)) 44 | 45 | Message = namedtuple('Message', 'header records') 46 | 47 | class MessageHeader(namedtuple('MessageHeader', 'id x timestamp y data_size')): 48 | __format__ = '<5I' 49 | def validate(self, scanner): 50 | pass 51 | 52 | Record = namedtuple('Record', 'n fields') 53 | 54 | class Field(namedtuple('Field', 'type code value')): 55 | INT = 0 56 | TYPE1 = 1 57 | STRING = 3 58 | BLOB = 4 59 | END_OF_RECORD = 5 60 | TYPE6 = 6 61 | 62 | 63 | class SkypeChatSyncScanner(object): 64 | def __init__(self, file_like_object, name=None): 65 | self.input = file_like_object 66 | self.name = name if name is not None else repr(self.input) 67 | 68 | def scan(self): 69 | size, self.file_header = self.scan_struct(FileHeader) 70 | self.timestamp = datetime.fromtimestamp(self.file_header.timestamp) 71 | self.warnings = 0 72 | self.blocks = [] 73 | size, self.blocks = self.scan_sequence(self.scan_block, self.file_header.data_size) 74 | self.validate() 75 | 76 | def validate(self): 77 | if len(self.blocks) != 6: 78 | warnings.warn("Incorrect number of blocks (%d) read from %s." % (len(self.blocks), self.name)) 79 | self.warnings += 1 80 | else: 81 | block_ids = [b.block_header.type for b in self.blocks] 82 | if sorted(block_ids) != range(1, 7): 83 | warnings.warn("Not all blocks 1..6 are present in %s." % self.name) 84 | self.warnings += 1 85 | block_6 = [b for b in self.blocks if b.block_header.type == 6] 86 | if len(block_6) != 1: 87 | raise ScanException("Block 6 not found, or more than one found in file %s." % self.name) 88 | 89 | def scan_sequence(self, method, nbytes, stop_at=lambda x: False): 90 | items = [] 91 | remaining = nbytes 92 | while remaining > 0: 93 | size, item = method(remaining) 94 | items.append(item) 95 | remaining -= size 96 | if stop_at(item): 97 | break 98 | if remaining < 0: 99 | warnings.warn("Invalid data size detected during sequence parsing in %s." % self.name) 100 | self.warnings += 1 101 | return nbytes - remaining, items 102 | 103 | def scan_struct(self, cls): 104 | size = calcsize(cls.__format__) 105 | data = self.input.read(size) 106 | if len(data) != size: 107 | raise ScanException("Error while scanning %s in %s. File too short." % (cls.__name__, self.name)) 108 | result = cls._make(unpack(cls.__format__, data)) 109 | result.validate(self) 110 | return size, result 111 | 112 | def scan_block(self, nbytes): 113 | hsize, block_header = self.scan_struct(BlockHeader) 114 | dsize, block_data = self.scan_block_data(block_header) 115 | return hsize + dsize, Block(block_header, block_data) 116 | 117 | def scan_block_data(self, block_header): 118 | if block_header.type == 5: 119 | return self.scan_block_5_data(block_header) 120 | elif block_header.type == 6: 121 | return self.scan_block_6_data(block_header) 122 | else: 123 | return self.scan_block_1_data(block_header) 124 | 125 | def scan_block_1_data(self, block_header): 126 | return self.scan_sequence(self.scan_record, block_header.data_size) 127 | 128 | def scan_block_5_data(self, block_header): 129 | return block_header.data_size, [unpack('<4I', self.input.read(16)) for i in range(block_header.data_size/16)] 130 | 131 | def scan_block_6_data(self, block_header): 132 | return self.scan_sequence(self.scan_message, block_header.data_size) 133 | 134 | def scan_record(self, nbytes): 135 | signature = self.input.read(1) 136 | if (signature != 'A'): 137 | raise ScanException("Record expected to start with 'A' in %s." % self.name) 138 | n = ord(self.input.read(1)) 139 | if n == 0: 140 | return 2, Record(n, []) 141 | else: 142 | size, fields = self.scan_sequence(self.scan_field, nbytes-2, lambda f: f.type == Field.END_OF_RECORD) 143 | return size + 2, Record(n, fields) 144 | 145 | def scan_field(self, nbytes): 146 | type = ord(self.input.read(1)) 147 | if type == Field.INT: 148 | csize, code = self.scan_7bitint() 149 | vsize, value = self.scan_7bitint() 150 | elif type == Field.STRING: 151 | csize, code = self.scan_7bitint() 152 | vsize, value = self.scan_cstring() 153 | elif type == Field.BLOB: 154 | csize, code = self.scan_7bitint() 155 | vsize, value = self.scan_blob() 156 | elif type == Field.TYPE1: 157 | csize, code = self.scan_7bitint() 158 | vsize, value = 8, self.input.read(8) 159 | elif type == Field.END_OF_RECORD: 160 | csize, code = self.scan_7bitint() 161 | vsize, value = 0, 0 162 | elif type == Field.TYPE6: 163 | code = self.input.read(1) # Seems to always be 0x08 164 | csize, oneortwo = self.scan_7bitint() # Seems to always be 1 or 2 165 | vsize = 1 166 | value = [] 167 | for i in range(oneortwo): 168 | _vsize, v = self.scan_7bitint() 169 | vsize += _vsize 170 | value.append(v) 171 | else: 172 | raise ScanException("Field of unexpected type %d detected in %s." % (type, self.name)) 173 | return csize + vsize + 1, Field(type, code, value) 174 | 175 | def scan_message(self, nbytes): 176 | hsize, header = self.scan_struct(MessageHeader) 177 | rsize, records = self.scan_sequence(self.scan_record, header.data_size) 178 | return hsize + rsize, Message(header, records) 179 | 180 | def scan_7bitint(self): 181 | result = 0 182 | coef = 1 183 | size = 0 184 | loop = True 185 | while loop: 186 | v = self.input.read(1) 187 | if (v == ''): 188 | raise ScanException("Error parsing 7 bit integer in %s. Unexpected end of file." % self.name) 189 | v = ord(v) 190 | if v & 0x80: 191 | v = v ^ 0x80 192 | else: 193 | loop = False 194 | result += v * coef 195 | coef <<= 7 196 | size += 1 197 | return size, result 198 | 199 | def scan_cstring(self): 200 | result = '' 201 | c = self.input.read(1) 202 | while c != '\x00' and c != '': 203 | result += c 204 | c = self.input.read(1) 205 | return len(result) + 1, result 206 | 207 | def scan_blob(self): 208 | sizesize, size = self.scan_7bitint() 209 | data = self.input.read(size) 210 | return sizesize + len(data), data 211 | 212 | 213 | ConversationMessage = namedtuple('ConversationMessage', 'timestamp author text is_edit') 214 | 215 | class SkypeChatSyncParser(object): 216 | def __init__(self, scanner): 217 | self.scanner = scanner 218 | 219 | def parse(self): 220 | self.timestamp = self.scanner.file_header.timestamp 221 | self.conversation = [] 222 | self.errors = 0 223 | self.is_empty = False 224 | if (len(self.scanner.blocks) == 0 or len(self.scanner.blocks[0].block_data) == 0 or len(self.scanner.blocks[0].block_data[0].fields) == 0): 225 | self.is_empty = True 226 | return 227 | participants = self.scanner.blocks[0].block_data[0].fields[0].value 228 | participants = participants.split(';')[0] 229 | participant1, participant2 = [name[1:] for name in participants.split('/')] 230 | self.participants = [participant1, participant2] 231 | 232 | # Find the first message with two parts - there we'll be able to detect the ID of the author of the conversation 233 | first_valid_block = -1 234 | for i, msg in enumerate(self.scanner.blocks[2].block_data): 235 | if len(msg.records) > 1 and len(msg.records[1].fields) > 1: 236 | first_valid_block = i 237 | break 238 | if first_valid_block == -1: 239 | self.is_empty = True 240 | return 241 | user1_id = self.scanner.blocks[2].block_data[first_valid_block].records[1].fields[1].value 242 | for msg in self.scanner.blocks[2].block_data: 243 | if len(msg.records) < 2: 244 | continue 245 | if len(msg.records[1].fields) < 3: 246 | continue 247 | user_id = msg.records[1].fields[1].value 248 | blob = msg.records[1].fields[2].value 249 | try: 250 | msg_start = blob.index('\x03\x02') 251 | msg_end = blob.index('\x00', msg_start+1) 252 | msg_text = blob[msg_start+2:msg_end] 253 | is_edit = False 254 | except: 255 | try: 256 | msg_start = blob.index('\x03"') 257 | msg_end = blob.index('\x00', msg_start+1) 258 | msg_text = blob[msg_start+2:msg_end] 259 | is_edit = True 260 | except: 261 | continue 262 | try: 263 | self.conversation.append(ConversationMessage(msg.header.timestamp, participant1 if user_id == user1_id else participant2, unicode(msg_text, 'utf-8'), is_edit)) 264 | except: 265 | self.errors += 1 266 | 267 | 268 | def parse_chatsync_file(filename): 269 | ''' 270 | Parses a given chatsync file. 271 | Throws an exception on any failure (which may happen even if the file is legitimate simply because we do not know all the details of the format). 272 | 273 | If succeeds, returns a SkypChatSyncParser object. Check out its "is_empty", "timestamp", "conversation" and "participants" fields. 274 | ''' 275 | with open(filename, 'rb') as f: 276 | s = SkypeChatSyncScanner(f) 277 | s.scan() 278 | p = SkypeChatSyncParser(s) 279 | p.parse() 280 | return p 281 | 282 | 283 | def parse_chatsync_profile_dir(dirname): 284 | ''' 285 | Looks for all *.dat files in a Skype profile's chatsync/ dir, 286 | returns a list of SkypeChatParser objects for those files that could be parsed successfully. 287 | ''' 288 | files = glob(os.path.join(dirname, "*", "*.dat")) 289 | results = [] 290 | for f in files: 291 | try: 292 | results.append(parse_chatsync_file(f)) 293 | except Exception, e: 294 | warnings.warn("Failed to parse file %s. Exception: %s" % (f, e.message)) 295 | return results --------------------------------------------------------------------------------