├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── build.py ├── build.sh ├── certifi ├── __init__.py ├── __main__.py ├── cacert.pem ├── core.py ├── old_root.pem └── weak.pem ├── classify.py ├── elasticsearch ├── elasticsearch.keystore ├── elasticsearch.yml └── jvm.options ├── extension ├── background.js ├── contentscript.js ├── ikke-1.4.zip ├── ikke.css ├── jquery-addons.js ├── jquery-ui.min.css ├── jquery-ui.min.js ├── jquery.js ├── manifest.json ├── octopus.jpg └── settings.js ├── facebooksdk ├── __init__.py └── version.py ├── graph.py ├── hosts.py ├── html ├── 3rd │ ├── d3.v4.js │ ├── d3.v4.min.js │ ├── jquery-3.2.1.min.js │ ├── jquery-ui-1.12.1 │ │ ├── AUTHORS.txt │ │ ├── LICENSE.txt │ │ ├── external │ │ │ └── jquery │ │ │ │ └── jquery.js │ │ ├── images │ │ │ ├── ui-icons_444444_256x240.png │ │ │ ├── ui-icons_555555_256x240.png │ │ │ ├── ui-icons_777620_256x240.png │ │ │ ├── ui-icons_777777_256x240.png │ │ │ ├── ui-icons_cc0000_256x240.png │ │ │ └── ui-icons_ffffff_256x240.png │ │ ├── index.html │ │ ├── jquery-ui.css │ │ ├── jquery-ui.js │ │ ├── jquery-ui.min.css │ │ ├── jquery-ui.min.js │ │ ├── jquery-ui.structure.css │ │ ├── jquery-ui.structure.min.css │ │ ├── jquery-ui.theme.css │ │ ├── jquery-ui.theme.min.css │ │ └── package.json │ ├── jstree-3.2.1.min.css │ └── jstree-3.2.1.min.js ├── extensions.html ├── graph.csv ├── graph.json ├── icons │ ├── blue-circle.png │ ├── browser-web-icon.png │ ├── calendar-icon.png │ ├── delete_icon.png │ ├── excel-xls-icon.png │ ├── facebook.png │ ├── favicon.ico │ ├── file-icon.png │ ├── git-icon.png │ ├── gmail-icon.png │ ├── hangouts-icon.png │ ├── keynote-icon.png │ ├── loading_spinner.gif │ ├── orange-circle.png │ ├── pdf-icon.png │ ├── person-icon.png │ ├── ppt-icon.png │ ├── rainbow-circle.png │ ├── rtf-icon.png │ ├── sad-computer.png │ ├── text-icon.png │ ├── tiff-icon.png │ ├── white_pixel.png │ └── word-doc-icon.png ├── index.html ├── main.css ├── main.js ├── projects.js ├── settings.css ├── settings.html └── settings.js ├── htmlparser.py ├── images ├── architecture.png ├── screenshot-context-menu.png ├── screenshot-grid.png ├── screenshot-ikke-dot.png ├── screenshot-ikke-graph.png ├── screenshot-ikke-related.png ├── screenshot-ikke-settings.png └── screenshot-ikke-statusbar.png ├── importers ├── __init__.py ├── browser.py ├── calendar.py ├── contact.py ├── download.py ├── file.py ├── git.py ├── gmail.py ├── gmail_credentials.json ├── google_apis.py ├── hangouts.py └── quickstart.py ├── installation └── ikke.plist ├── installer.py ├── localsearch └── search.vbs ├── main.py ├── memory.py ├── poller.py ├── preferences.py ├── pubsub.py ├── pyinstaller.spec ├── server.py ├── server.spec ├── settings.py ├── setup.py ├── simple_logging.py ├── stopwords.py ├── storage.py ├── threadpool.py ├── utils.py └── words.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/*.pyc 2 | *.pyc 3 | token.pickle 4 | .vscode/.ropeproject/config.py 5 | .vscode/.ropeproject/objectdb 6 | venv/* 7 | build/* 8 | dist/* 9 | .eggs/* 10 | exe.egg-info/* 11 | install.egg-info/* 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": true, 3 | "python.linting.enabled": true 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | NON-COMMERCIAL IKKE License 2 | 3 | Copyright (c) 2017-2021 Chris Laffra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to use, 7 | copy, and modify the software for Personal Use. 8 | 9 | This license is subject to the following conditions: 10 | 11 | Without limiting other conditions in the License, the grant of rights under 12 | the License will not include, and the License does not grant to you, the 13 | right to Sell the Software. 14 | 15 | For purposes of the foregoing, “Sell” means practicing any or all of the 16 | rights granted to you under the License to provide to third parties, for 17 | a fee or other consideration (including without limitation fees for 18 | hosting or consulting/ support services related to the Software), a product 19 | or service whose value derives, entirely or substantially, from the 20 | functionality of the Software. 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ikke 2 | Easily find back all your data! 3 | 4 | # Features 5 | 6 | Ikke produces a local backup of your (social) data, with fast local search, 7 | visualizing the connections between de data across various sources. 8 | 9 | Why the name Ikke? 10 | 11 | Ikke is a Dutch colloquial/slang word, meaning "I", "Me", or "Myself". It is pronounced as "ik-kuh", and is mainly used in the Dutch pseudo-narcist phrase "Ikke, ikke, ikke, en de rest kan stikken", translated roughly into "Me, me, me, and screw all others". 12 | 13 | When [pronounced correctly](https://upload.wikimedia.org/wikipedia/commons/3/39/Nl-ikke.ogg), 14 | Ikke sounds a lot like the English word "ticket", if you remove the leading and trailing t's. 15 | 16 | # How it works 17 | 18 |
19 | 20 |
21 | 22 | Ikke consists of various components: 23 | * Importers for data sources such as browser history, local downloads, gmail, git, etc. 24 | * A Chrome browser extension that: 25 | * collects a thumbnail image of websites you visited to find them back quickly. 26 | * shows related items to the current "Essence" of the site being viewed, see the yellow Dot in web pages. 27 | * A statusbar icon showing the number of related items with a menu. 28 | * A super-fast freetext search backend that runs on your local machine, not needing any cloud access. 29 | * Smooth graph visualizations that show relationships between various data sources. 30 | * A [Settings UI](http://localhost:1964/settings) to control the index and indicate if you want to use the Dot or not. 31 | 32 | # Installation 33 | 34 | Setup Ikke: 35 | * Clone the repo and cd to its root 36 | ``` 37 | git clone https://github.com/laffra/Ikke 38 | cd Ikke 39 | ``` 40 | * Set up a virtualenv 41 | ``` 42 | python3 -m pip install virtualenv 43 | python3 -m venv env_ikke 44 | ``` 45 | * Activate the virtualenv you just created 46 | ``` 47 | source env_ikke/bin/activate 48 | ``` 49 | * Install elasticsearch and the python dependencies: 50 | ``` 51 | python3 setup.py install 52 | ``` 53 | * Visit "chrome://extensions" in your browser 54 | * Load the unpacked extension from the repo's "extension" folder. 55 | 56 | The first time Ikke runs, it performs post-setup tasks. One of the things Ikke needs is your approval to index your Google data, such as gmail, calendar, etc. Things that happen during the first run: 57 | * A Google dialog is shown to give "Ikke Graph" access to your data. 58 | * The "Ikke graph" app is not verified by Google, so you may get a scary warning. This is OK. Click on the 59 | "advanced" link and provide access. See below what happens next. 60 | * The auth token received from Google is stored on your local machine under ~/IKKE. 61 | 62 | Your privacy is preserved: 63 | * The "Ikke Graph" app can only access your data using the locally stored token. Therefore, it can only index your data on your local machine. 64 | * The token and all your indexed data are stored locally only. No one will have access to it, unless they run a program on your local machine and use the token. 65 | * You will get an email and/or notification from Google saying "Ikke Graph was granted access to your Google Account" 66 | 67 | # Usage 68 | 69 | Run Ikke: 70 | ``` 71 | python3 main.py 72 | ``` 73 | 74 | Visit your settings: 75 | * Click on the statusbar number icon or see [Settings](http://localhost:1964/settings): 76 | 77 |
78 | 79 |
80 | 81 | * By default, Ikke will index your browser history. 82 | * To load other sources, such as gmail, hit the corresponding "Load" button. 83 | * Optional: Enable the browser extension's "Dot". 84 | * Click on the IKKE logo to start a new search on the currently indexed data. 85 | 86 | # Using the Dot 87 | 88 | Use the "Dot" to show related items: 89 | * Enable the feature in your settings (see above). 90 | * Go to a website and notice the dot appear, such as the one shown below next to Elon Musk saying "12": 91 | 92 |
93 | 94 |
95 | 96 | * Click on the dot to discover the 12 related items to Elon Musk (this list is different for every user, of course): 97 | 98 |
99 | 100 |
101 | 102 | # Showing an Ikke Graph 103 | 104 | You can show an Ikke Graph from the Dot, the statusbar, and from its UI: 105 | 106 |
107 | 108 |
109 | 110 | # Showing an Ikke Grid 111 | 112 | The graph is great to see a quick overview of the relationships between the data. 113 | If you want to focus more on time and recency, the Ikke Grid may be a better UI: 114 | 115 |
116 | 117 |
118 | 119 | The Grid and the Graph show the same information, sorted differently. 120 | 121 | # Using the status bar icon 122 | 123 | You can always use the statusbar icon to explore related items as well: 124 | 125 |
126 | 127 |
128 | 129 | # Using the context menu 130 | 131 | Inside Chrome, the Ikke Dot tries to guess the current "essence" of the page. It favors words 132 | that appear in the top-middle of the page and that have more "weight" than others. 133 | Sometimes, however, you will want to search for a word nearby. Simply right-mouse click on it, 134 | or select some text first, and activate the context menu to use the Ikke menu item: 135 | 136 |
137 | 138 |
139 | 140 | # Uninstall 141 | 142 | Uninstall takes three steps: 143 | * Visit [Settings](http://localhost:1964/settings) and delete all data. 144 | * Remove ~/IKKE entirely 145 | * Remove the local repo you cloned during startup 146 | 147 | # Future Work 148 | 149 | Some things that could improve Ikke: 150 | * Add authentication to elasticsearch 151 | * Add an ML model to the Essence finder 152 | * Finish up the py2app bundling (see build.py) 153 | * Distribute as a Mac app in the AppStore with a real installer 154 | * Handle possible port number conflicts 155 | * Add more importers, such as TikTok, FB, IG, and WhatsApp... 156 | * Consider extensions for other browsers, such as IE, Firefox 157 | * Add a plugin to standalone tools, such as VSCode. 158 | 159 | # Privacy 160 | 161 | You privacy is key. Ikke does not upload ANY data. Everything being indexed is stored on your local machine. Ikke does not use your data for marketing purposes or ads. No logging is ever uploaded. Your data is yours and stays yours. 162 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | from distutils import cmd 3 | from setuptools import setup, find_packages 4 | from setuptools.command.install import install 5 | from subprocess import check_call 6 | import py2app 7 | 8 | APP = ['main.py'] 9 | DATA_FILES = [] 10 | OPTIONS = { 11 | 'argv_emulation': True, 12 | 'plist': { 13 | 'LSUIElement': True, 14 | }, 15 | 'packages': ['rumps'], 16 | } 17 | 18 | setup( 19 | app = APP, 20 | data_files = DATA_FILES, 21 | options = {'py2app': OPTIONS}, 22 | setup_requires = ['py2app'], 23 | ) 24 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | clear 2 | 3 | echo "Step 1: Stop existing Ikke instances..." 4 | killall ikke 5 | 6 | echo "Step 2: Clean the old distribution..." 7 | cd /Users/laffra/dev/Ikke && rm -rf dist build 8 | 9 | echo "Step 3: Create new distribution..." 10 | pyinstaller pyinstaller.spec 11 | 12 | echo "Step 4: Launch to test..." 13 | open dist/ikke -------------------------------------------------------------------------------- /certifi/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import where, old_where 2 | 3 | __version__ = "2017.11.05" 4 | -------------------------------------------------------------------------------- /certifi/__main__.py: -------------------------------------------------------------------------------- 1 | from certifi import where 2 | print(where()) 3 | -------------------------------------------------------------------------------- /certifi/core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | certifi.py 6 | ~~~~~~~~~~ 7 | 8 | This module returns the installation location of cacert.pem. 9 | """ 10 | import os 11 | import warnings 12 | 13 | 14 | class DeprecatedBundleWarning(DeprecationWarning): 15 | """ 16 | The weak security bundle is being deprecated. Please bother your service 17 | provider to get them to stop using cross-signed roots. 18 | """ 19 | 20 | 21 | def where(): 22 | f = os.path.dirname(__file__) 23 | 24 | return os.path.join(f, 'cacert.pem') 25 | 26 | 27 | def old_where(): 28 | warnings.warn( 29 | "The weak security bundle is being deprecated. It will be removed in " 30 | "2018.", 31 | DeprecatedBundleWarning 32 | ) 33 | f = os.path.dirname(__file__) 34 | return os.path.join(f, 'weak.pem') 35 | 36 | if __name__ == '__main__': 37 | print(where()) 38 | -------------------------------------------------------------------------------- /classify.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import itertools 3 | import logging 4 | import stopwords 5 | import storage 6 | 7 | MOST_COMMON_COUNT = 41 8 | ADD_CONTENT_LABELS = False 9 | 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def shorten(label): 15 | if len(label) > 31: 16 | label = label[:13] + ' ... ' + label[-13:] 17 | return label 18 | 19 | 20 | class Label(storage.Data): 21 | def __init__(self, name): 22 | super(Label, self).__init__(name) 23 | self.kind = 'label' 24 | self.color = '#888' 25 | self.font_size = 12 26 | 27 | def is_related_item(self, other): 28 | return self.label in other.words 29 | 30 | def is_duplicate(self, duplicates): 31 | if self.label in duplicates: 32 | return True 33 | duplicates.add(self.label) 34 | return False 35 | 36 | 37 | class TooMuch(storage.Data): 38 | def __init__(self, count): 39 | super(TooMuch, self).__init__('Search is too broad: %d results not shown.' % count) 40 | self.kind = 'label' 41 | self.color = 'red' 42 | self.font_size = 48 43 | 44 | 45 | def add_contact(contact, contacts): 46 | if contact.label in contacts: 47 | contact = contacts[contact.label] 48 | else: 49 | contacts[contact.label] = contact 50 | return contact 51 | 52 | 53 | def get_persons(items): 54 | return [(contact, item) for item in items for contact in item.persons] 55 | 56 | 57 | def adjacent(items): 58 | for index in range(len(items) - 2): 59 | yield (items[index], items[index + 1]) 60 | 61 | 62 | def get_item_edges(items): 63 | # type(list, str) -> list 64 | timestamps = [item.timestamp for item in items if item.timestamp] 65 | if timestamps: 66 | storage.TimeNode.set_timerange(min(timestamps), max(timestamps)) 67 | return { 68 | (item, related) 69 | for item in items 70 | for related in item.get_related_items() 71 | } 72 | 73 | 74 | def remove_duplicates(items, keep_duplicates): 75 | # type(list, bool) -> list 76 | duplicates = set() 77 | sorted_items = sorted(items, key=lambda item: -(item.timestamp or 0)) 78 | results = [item for item in sorted_items if keep_duplicates or not item.is_duplicate(duplicates)] 79 | if len(results) > storage.MAX_NUMBER_OF_ITEMS: 80 | too_much = TooMuch(len(results) - storage.MAX_NUMBER_OF_ITEMS) 81 | results = results[:storage.MAX_NUMBER_OF_ITEMS] 82 | results.append(too_much) 83 | return results 84 | 85 | 86 | def get_most_common_words(query, items): 87 | counter = collections.Counter() 88 | for item in items: 89 | item.update_words(items) 90 | counter.update([ 91 | word 92 | for word in item.words 93 | if word and word != query 94 | ]) 95 | for word in query.lower().split(' '): 96 | if word in counter: 97 | del counter[word] 98 | most_common = {key for key, count in counter.most_common(MOST_COMMON_COUNT)} 99 | return most_common 100 | 101 | 102 | 103 | def get_edges(query, items, add_words=False, keep_duplicates=False): 104 | # type(str, list, str, bool) -> (list, list) 105 | edges = get_item_edges(items) 106 | if add_words: 107 | items += [Label(word) for word in get_most_common_words(query, items) if not stopwords.is_stopword(word)] 108 | for item1, item2 in itertools.combinations(items, 2): 109 | if item1.is_related_item(item2) or item2.is_related_item(item1): 110 | item1.edges += 1 111 | item2.edges += 1 112 | logger.debug(" - add edge %s %s %s - %s" % ( item1.is_related_item(item2), item2.is_related_item(item1), item1, item2)) 113 | edges.add((item1, item2)) 114 | items = remove_duplicates(items, keep_duplicates) 115 | edges = [edge for edge in edges if edge[0] in items and edge[1] in items] 116 | logger.info("Created graph for %d edges and %d items, with %d emails" % (len(edges), len(items), len(list(filter(lambda item: item.kind == "gmail", items))))) 117 | return list(edges), items 118 | 119 | 120 | def debug_results(labels, items): 121 | show_details = False 122 | level = logging.get_level() 123 | logging.set_level(logging.DEBUG) 124 | logging.debug('found %d labels with %d items' % (len(labels), len(items))) 125 | logging.debug('Included:') 126 | 127 | def shorten(x): 128 | if isinstance(x, str): return x.replace('\n', ' ') 129 | return x 130 | 131 | for item in items: 132 | logging.debug(' %s %s %s' % (item.kind, repr(item.label), item.uid)) 133 | if show_details: 134 | for var in vars(item): 135 | logging.debug(' %s: %s' % (var, shorten(getattr(item, var)))) 136 | for k,v in labels.items(): 137 | logging.debug(k.label) 138 | for item in v: 139 | logging.debug (' %s %s' % (item.kind, item.label)) 140 | logging.debug('Removed:') 141 | for item in set(all_items) - set(items): 142 | logging.debug(' %s %s %s' % (item.kind, repr(item.label), item.uid)) 143 | if show_details: 144 | for var in vars(item): 145 | logging.debug(' %s: %s' % (var, shorten(getattr(item, var)))) 146 | logging.set_level(level) 147 | 148 | 149 | 150 | if __name__ == '__main__': 151 | level = logging.get_level() 152 | logging.set_level(logging.DEBUG) 153 | query = 'blockchain' 154 | items = storage.Storage.search(query, 3) 155 | logging.debug('Found %d items.' % len(items)) 156 | edges, all_items = get_edges(items) 157 | logging.debug('Edges:') 158 | for item1, item2 in edges: 159 | logging.debug(' %s - %s' % (repr(item1.label), repr(item2.label))) 160 | logging.debug('Items:') 161 | for item in items: 162 | logging.debug(' %s' % repr(item.label)) 163 | -------------------------------------------------------------------------------- /elasticsearch/elasticsearch.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/elasticsearch/elasticsearch.keystore -------------------------------------------------------------------------------- /elasticsearch/elasticsearch.yml: -------------------------------------------------------------------------------- 1 | xpack.security.enabled: true 2 | discovery.type: single-node 3 | -------------------------------------------------------------------------------- /elasticsearch/jvm.options: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/elasticsearch/jvm.options -------------------------------------------------------------------------------- /extension/background.js: -------------------------------------------------------------------------------- 1 | var email = "???"; 2 | 3 | const settings = { 4 | "debug-browser-extension": "", 5 | "show-ikke-dot": "", 6 | } 7 | 8 | chrome.identity.getProfileUserInfo(function(info) { 9 | email = info.email; 10 | }); 11 | 12 | chrome.extension.onMessage.addListener(function(request, sender, sendResponse) { 13 | if (request.kind == "ikke-email") { 14 | sendResponse({email: email}); 15 | } 16 | }); 17 | 18 | function call(url, handler) { 19 | var xhr = new XMLHttpRequest(); 20 | xhr.open("GET", url, true); 21 | xhr.addEventListener("load", function() { 22 | handler(this.responseText); 23 | }); 24 | xhr.send(); 25 | } 26 | 27 | chrome.runtime.onMessage.addListener( 28 | function(request, sender, sendResponse) { 29 | if (!sender.tab.active) return; 30 | switch (request.type) { 31 | case 'get-related-items': 32 | call('http://localhost:1964/get_related_items' + 33 | '?url=' + encodeURIComponent(request.url) + 34 | '&title=' + encodeURIComponent(request.title) + 35 | '&essence=' + encodeURIComponent(request.essence) + 36 | '&email=' + encodeURIComponent(email) + 37 | '&image=' + encodeURIComponent(request.image) + 38 | '&selection=' + encodeURIComponent(request.selection) + 39 | '&favicon=' + encodeURIComponent(request.favicon) + 40 | '&keywords=' + encodeURIComponent(request.keywords || ''), function(response) { 41 | sendResponse(JSON.parse(response)); 42 | }); 43 | break; 44 | } 45 | return true; 46 | } 47 | ); 48 | 49 | function syncSetting(key) { 50 | call('http://localhost:1964/settings_get?key=' + key, function(response) { 51 | if (settings[key] != response) { 52 | settings[key] = response; 53 | sendMessage({ type: key, value: settings[key] }); 54 | } 55 | }, true); 56 | } 57 | 58 | setInterval(function() { 59 | for (key in settings) { 60 | syncSetting(key); 61 | } 62 | }, 1000); 63 | 64 | function sendMessage(data) { 65 | chrome.tabs.query({}, function(tabs) { 66 | for (tab of tabs) { 67 | console.log("send", tab.id, data.kind); 68 | chrome.tabs.sendMessage(tab.id, data, function(response) { 69 | console.log(tab.id, response); 70 | }); 71 | } 72 | }); 73 | } 74 | 75 | function notifyTabs(activeInfo) { 76 | setTimeout(function() { 77 | for (key in settings) { 78 | sendMessage({ type: key, value: settings[key] }); 79 | } 80 | sendMessage({ type: "tab-changed" }); 81 | }, 100); 82 | } 83 | 84 | chrome.tabs.onUpdated.addListener(function(tabId) { notifyTabs({ tabId })}); 85 | chrome.tabs.onActivated.addListener(notifyTabs); 86 | 87 | chrome.contextMenus.create({ 88 | title: "Search Ikke for \"%s\"", 89 | contexts: ["selection"], 90 | onclick: function(info, tab) { 91 | console.log("search ikke", info); 92 | window.open('http://localhost:1964/?q=' + info.selectionText); 93 | }, 94 | }); -------------------------------------------------------------------------------- /extension/ikke-1.4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/extension/ikke-1.4.zip -------------------------------------------------------------------------------- /extension/ikke.css: -------------------------------------------------------------------------------- 1 | .ikke_sidebar { 2 | } -------------------------------------------------------------------------------- /extension/jquery-addons.js: -------------------------------------------------------------------------------- 1 | 2 | $.fn.isInViewport = function(){ 3 | var win = $(window); 4 | var viewport = { 5 | top : win.scrollTop(), 6 | left : win.scrollLeft() 7 | }; 8 | viewport.right = viewport.left + win.width(); 9 | viewport.bottom = viewport.top + win.height(); 10 | var bounds = this.offset(); 11 | bounds.right = bounds.left + this.outerWidth(); 12 | bounds.bottom = bounds.top + this.outerHeight(); 13 | return (!(viewport.right < bounds.left || viewport.left > bounds.right || viewport.bottom < bounds.top || viewport.top > bounds.bottom)); 14 | }; 15 | -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Chris Laffra - laffra@gmail.com", 3 | "background": { 4 | "scripts": [ "background.js" ] 5 | }, 6 | "content_scripts": [ { 7 | "all_frames": true, 8 | "js": [ "jquery.js", "jquery-addons.js", "jquery-ui.min.js", "settings.js", "contentscript.js" ], 9 | "css": [ "jquery-ui.min.css", "ikke.css" ], 10 | "matches": [ "http://*/*", "https://*/*" ] 11 | } ], 12 | "description": "Ikke", 13 | "manifest_version": 2, 14 | "name": "Ikke", 15 | "icons": { 16 | "16": "octopus.jpg", 17 | "48": "octopus.jpg", 18 | "128": "octopus.jpg" 19 | }, 20 | "page_action": { 21 | "default_icon": { 22 | "38": "octopus.jpg" 23 | }, 24 | "default_title": "Ikke" 25 | }, 26 | "permissions": [ "tabs", "http://*/", "https://*/", "contextMenus", "identity", "identity.email" ], 27 | "short_name": "Ikke", 28 | "update_url": "https://clients2.google.com/service/update2/crx", 29 | "version": "1.8" 30 | } 31 | -------------------------------------------------------------------------------- /extension/octopus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/extension/octopus.jpg -------------------------------------------------------------------------------- /extension/settings.js: -------------------------------------------------------------------------------- 1 | if (document.location.href.startsWith('http://localhost:')) { 2 | $('#ikke-extension').css('display', 'none'); 3 | $('#ikke-gmail-needed').css('display', 'block'); 4 | $('#ikke-settings').css('display', 'block'); 5 | } 6 | -------------------------------------------------------------------------------- /facebooksdk/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 Mobolic 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | __version__ = "3.0.0-alpha" 18 | -------------------------------------------------------------------------------- /graph.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from collections import defaultdict 3 | import logging 4 | from random import random 5 | from re import I 6 | from urllib.parse import unquote 7 | import json 8 | import time 9 | from importers import browser 10 | from importers import contact 11 | from importers import gmail 12 | import classify 13 | from preferences import ChromePreferences 14 | import os 15 | import utils 16 | from threadpool import ThreadPool 17 | from storage import Storage 18 | 19 | days = { 20 | 'day': 1, 21 | 'week': 7, 22 | 'month': 31, 23 | 'month3': 92, 24 | 'month6': 182, 25 | 'year': 365, 26 | 'forever': 3650, 27 | } 28 | MY_EMAIL_ADDRESS = ChromePreferences().get_email() 29 | LINE_COLORS = [ '#f4c950', '#ee4e5a', '#489ac9', '#41ba7d', '#fb7c54',] * 2 30 | 31 | ALL_ITEM_KINDS = [ 'all', 'contact', 'gmail', 'calendar', 'git', 'hangouts', 'browser', 'file', ] 32 | MY_ITEM_KINDS = [ 'contact', 'gmail', 'calendar', 'git', 'hangouts', 'browser', 'file' ] 33 | 34 | MAX_LABEL_LENGTH = 42 35 | ADD_WORDS_MINIMUM_COUNT = 100 36 | REDUCE_GRAPH_SIZE = False 37 | 38 | IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'tiff', 'png', 'raw'] 39 | 40 | logging.basicConfig(level=logging.INFO) 41 | logger = logging.getLogger(__name__) 42 | 43 | class Graph: 44 | def __init__(self, email, query, duration_string): 45 | logger.info('GRAPH: init %s %s' % (repr(query), duration_string)) 46 | self.email = email 47 | self.query = query 48 | self.duration_string = duration_string 49 | self.search_count = {} 50 | self.search_results = defaultdict(list) 51 | self.search_duration = defaultdict(list) 52 | self.time_nodes = set() 53 | self.my_pool = ThreadPool(1, [ 54 | (self.search, days[duration_string]) 55 | ]) 56 | 57 | def add_result(self, kind, count, items, duration): 58 | self.search_results[kind] = set(filter(None, self.search_results[kind] + list(items))) 59 | self.search_count[kind] = count 60 | self.search_duration[kind].append(duration) 61 | logger.debug('found %d of %s items' % (len(items), kind)) 62 | 63 | def search(self, days): 64 | start_time = time.time() 65 | all_items = list(filter(None, Storage.search(unquote(self.query), days))) 66 | duration = time.time() - start_time 67 | 68 | self.add_result('all', len(all_items), all_items, duration) 69 | for kind in MY_ITEM_KINDS: 70 | items = [item for item in all_items if item.kind in ('label', kind)] 71 | self.add_result(kind, len(items), items, duration) 72 | 73 | def get_graph(self, kind, keep_duplicates): 74 | self.my_pool.wait_completion() 75 | all_found_items = self.search_results['all' if kind in ['contact', 'file'] else kind] 76 | found_items = [item for item in all_found_items if item.kind != 'contact' or item.label != self.email] 77 | add_words = True 78 | edges, items = classify.get_edges(self.query, found_items, add_words, keep_duplicates) 79 | removed_item_count = 0 80 | for item in found_items: 81 | if not item in items: 82 | removed_item_count += 1 83 | if kind in ['contact', 'file']: 84 | items = [item for item in items if item.kind == kind] 85 | if REDUCE_GRAPH_SIZE: 86 | items = self.remove_lonely_images(items) 87 | items = self.remove_lonely_labels(items) 88 | # items = self.remove_labels(items) 89 | 90 | for item in items: 91 | if len(item.label) > MAX_LABEL_LENGTH: 92 | cutoff = round(MAX_LABEL_LENGTH / 2) 93 | head = item.label[:cutoff] 94 | tail = item.label[-cutoff:] 95 | item.label = "%s...%s" % (head, tail) 96 | item.date = str(datetime.datetime.fromtimestamp(float(item.timestamp))) if item.timestamp else "" 97 | item.x = random() * 500 98 | item.y = random() * 500 99 | 100 | nodes_index = dict((item.uid, n) for n, item in enumerate(items)) 101 | label_index = dict((item.label, n) for n, item in enumerate(items)) 102 | nodes = [vars(item) for item in items] 103 | def get_color(item): 104 | return LINE_COLORS[label_index.get(item.label, 0) % len(LINE_COLORS)] 105 | links = [ 106 | { 107 | 'source': nodes_index[item1.uid], 108 | 'target': nodes_index[item2.uid], 109 | 'color': get_color(item2), 110 | 'stroke': 1, 111 | } 112 | for item1, item2 in edges 113 | if item1 and item1.uid in nodes_index and item2 and item2.uid in nodes_index 114 | ] 115 | 116 | logger.debug("Found %d nodes" % len(nodes)) 117 | for node in nodes: 118 | logger.debug(" %s" % node["kind"]) 119 | logger.debug("Found %d links" % len(links)) 120 | 121 | stats = { 122 | 'found': len(found_items), 123 | 'removed': removed_item_count, 124 | 'memory': utils.get_memory() 125 | } 126 | stats.update(Storage.stats) 127 | logger.info("Graph stats: %s" % json.dumps(stats)) 128 | if kind == 'all': 129 | browser.cleanup() 130 | gmail.cleanup() 131 | contact.cleanup() 132 | Storage.stats.clear() 133 | 134 | return { 135 | 'graph': [], 136 | 'links': links, 137 | 'nodes': nodes, 138 | 'directed': False, 139 | 'stats': stats 140 | } 141 | 142 | def remove_labels(self, items): 143 | return [item for item in items if item.kind != 'label'] 144 | 145 | 146 | def remove_lonely_images(self, items): 147 | return [item for item in items if not self.is_lonely_image(item)] 148 | 149 | 150 | def remove_lonely_labels(self, items): 151 | return [item for item in items if not self.is_lonely_label(item)] 152 | 153 | 154 | def is_lonely_image(self, item): 155 | if item.kind != "file": 156 | return False 157 | _, extension = os.path.splitext(item.path) 158 | return extension[1:].lower() in IMAGE_EXTENSIONS 159 | 160 | 161 | def is_lonely_label(self, item): 162 | return item.kind == "label" and item.edges == 0 163 | -------------------------------------------------------------------------------- /hosts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | def get_etc_host_path(): 6 | if os.name == 'nt': 7 | return os.path.join(os.path.sep, 'etc', 'hosts') 8 | else: 9 | return os.path.join(os.path.sep, 'private', 'etc', 'hosts') 10 | 11 | 12 | def setup(remote_url, local_url): 13 | with open(get_etc_host_path()) as fin: 14 | contents = fin.read() 15 | rule = '%s %s' % (local_url, remote_url) 16 | if rule not in contents: 17 | print('Adding rule to map %s to %s' % (remote_url, local_url)) 18 | with open(get_etc_host_path(), 'a') as fout: 19 | fout.write('\n') 20 | fout.write(rule) 21 | fout.write('\n') 22 | clear_DNS() 23 | else: 24 | print('Rule to map %s to %s already present' % (remote_url, local_url)) 25 | 26 | 27 | def clear_DNS(): 28 | if os.name == 'nt': 29 | pass 30 | else: 31 | print('killall -HUP mDNSResponder') 32 | os.system('sudo dscacheutil -flushcache') 33 | os.system('sudo killall -HUP mDNSResponder') 34 | # send user to chrome://net-internals/#dns and clear the cache? 35 | 36 | 37 | def setup_as_administrator(): 38 | if os.name == 'nt': 39 | pass 40 | else: 41 | script = os.path.join(os.getcwd(), "hosts.py") 42 | print("") 43 | print("To set up IKKE correctly, we need to add an extra DNS entry to your /private/etc/hosts file.") 44 | print("") 45 | print("This is the script being executed: %s" % script) 46 | print("") 47 | print("Please provide your adminstrator password.") 48 | print("") 49 | command = 'sudo %s %s' % (sys.executable, script) 50 | os.system('osascript -e \'tell application "Terminal" to do script "%s"\'' % command) 51 | print("") 52 | print("This script is launched in another Terminal (use Cmd+Tab to find it).") 53 | 54 | 55 | if __name__ == '__main__': 56 | print('Setting up etc/hosts rule for Ikke') 57 | try: 58 | setup('ikke', '127.0.0.1:1964') 59 | except PermissionError as e: 60 | print("Switching to sudo") 61 | setup_as_administrator() 62 | -------------------------------------------------------------------------------- /html/3rd/jquery-ui-1.12.1/AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Authors ordered by first contribution 2 | A list of current team members is available at http://jqueryui.com/about 3 | 4 | Paul Bakaus 5 | Richard Worth 6 | Yehuda Katz 7 | Sean Catchpole 8 | John Resig 9 | Tane Piper 10 | Dmitri Gaskin 11 | Klaus Hartl 12 | Stefan Petre 13 | Gilles van den Hoven 14 | Micheil Bryan Smith 15 | Jörn Zaefferer 16 | Marc Grabanski 17 | Keith Wood 18 | Brandon Aaron 19 | Scott González 20 | Eduardo Lundgren 21 | Aaron Eisenberger 22 | Joan Piedra 23 | Bruno Basto 24 | Remy Sharp 25 | Bohdan Ganicky 26 | David Bolter 27 | Chi Cheng 28 | Ca-Phun Ung 29 | Ariel Flesler 30 | Maggie Wachs 31 | Scott Jehl 32 | Todd Parker 33 | Andrew Powell 34 | Brant Burnett 35 | Douglas Neiner 36 | Paul Irish 37 | Ralph Whitbeck 38 | Thibault Duplessis 39 | Dominique Vincent 40 | Jack Hsu 41 | Adam Sontag 42 | Carl Fürstenberg 43 | Kevin Dalman 44 | Alberto Fernández Capel 45 | Jacek Jędrzejewski (http://jacek.jedrzejewski.name) 46 | Ting Kuei 47 | Samuel Cormier-Iijima 48 | Jon Palmer 49 | Ben Hollis 50 | Justin MacCarthy 51 | Eyal Kobrigo 52 | Tiago Freire 53 | Diego Tres 54 | Holger Rüprich 55 | Ziling Zhao 56 | Mike Alsup 57 | Robson Braga Araujo 58 | Pierre-Henri Ausseil 59 | Christopher McCulloh 60 | Andrew Newcomb 61 | Lim Chee Aun 62 | Jorge Barreiro 63 | Daniel Steigerwald 64 | John Firebaugh 65 | John Enters 66 | Andrey Kapitcyn 67 | Dmitry Petrov 68 | Eric Hynds 69 | Chairat Sunthornwiphat 70 | Josh Varner 71 | Stéphane Raimbault 72 | Jay Merrifield 73 | J. Ryan Stinnett 74 | Peter Heiberg 75 | Alex Dovenmuehle 76 | Jamie Gegerson 77 | Raymond Schwartz 78 | Phillip Barnes 79 | Kyle Wilkinson 80 | Khaled AlHourani 81 | Marian Rudzynski 82 | Jean-Francois Remy 83 | Doug Blood 84 | Filippo Cavallarin 85 | Heiko Henning 86 | Aliaksandr Rahalevich 87 | Mario Visic 88 | Xavi Ramirez 89 | Max Schnur 90 | Saji Nediyanchath 91 | Corey Frang 92 | Aaron Peterson 93 | Ivan Peters 94 | Mohamed Cherif Bouchelaghem 95 | Marcos Sousa 96 | Michael DellaNoce 97 | George Marshall 98 | Tobias Brunner 99 | Martin Solli 100 | David Petersen 101 | Dan Heberden 102 | William Kevin Manire 103 | Gilmore Davidson 104 | Michael Wu 105 | Adam Parod 106 | Guillaume Gautreau 107 | Marcel Toele 108 | Dan Streetman 109 | Matt Hoskins 110 | Giovanni Giacobbi 111 | Kyle Florence 112 | Pavol Hluchý 113 | Hans Hillen 114 | Mark Johnson 115 | Trey Hunner 116 | Shane Whittet 117 | Edward A Faulkner 118 | Adam Baratz 119 | Kato Kazuyoshi 120 | Eike Send 121 | Kris Borchers 122 | Eddie Monge 123 | Israel Tsadok 124 | Carson McDonald 125 | Jason Davies 126 | Garrison Locke 127 | David Murdoch 128 | Benjamin Scott Boyle 129 | Jesse Baird 130 | Jonathan Vingiano 131 | Dylan Just 132 | Hiroshi Tomita 133 | Glenn Goodrich 134 | Tarafder Ashek-E-Elahi 135 | Ryan Neufeld 136 | Marc Neuwirth 137 | Philip Graham 138 | Benjamin Sterling 139 | Wesley Walser 140 | Kouhei Sutou 141 | Karl Kirch 142 | Chris Kelly 143 | Jason Oster 144 | Felix Nagel 145 | Alexander Polomoshnov 146 | David Leal 147 | Igor Milla 148 | Dave Methvin 149 | Florian Gutmann 150 | Marwan Al Jubeh 151 | Milan Broum 152 | Sebastian Sauer 153 | Gaëtan Muller 154 | Michel Weimerskirch 155 | William Griffiths 156 | Stojce Slavkovski 157 | David Soms 158 | David De Sloovere 159 | Michael P. Jung 160 | Shannon Pekary 161 | Dan Wellman 162 | Matthew Edward Hutton 163 | James Khoury 164 | Rob Loach 165 | Alberto Monteiro 166 | Alex Rhea 167 | Krzysztof Rosiński 168 | Ryan Olton 169 | Genie <386@mail.com> 170 | Rick Waldron 171 | Ian Simpson 172 | Lev Kitsis 173 | TJ VanToll 174 | Justin Domnitz 175 | Douglas Cerna 176 | Bert ter Heide 177 | Jasvir Nagra 178 | Yuriy Khabarov <13real008@gmail.com> 179 | Harri Kilpiö 180 | Lado Lomidze 181 | Amir E. Aharoni 182 | Simon Sattes 183 | Jo Liss 184 | Guntupalli Karunakar 185 | Shahyar Ghobadpour 186 | Lukasz Lipinski 187 | Timo Tijhof 188 | Jason Moon 189 | Martin Frost 190 | Eneko Illarramendi 191 | EungJun Yi 192 | Courtland Allen 193 | Viktar Varvanovich 194 | Danny Trunk 195 | Pavel Stetina 196 | Michael Stay 197 | Steven Roussey 198 | Michael Hollis 199 | Lee Rowlands 200 | Timmy Willison 201 | Karl Swedberg 202 | Baoju Yuan 203 | Maciej Mroziński 204 | Luis Dalmolin 205 | Mark Aaron Shirley 206 | Martin Hoch 207 | Jiayi Yang 208 | Philipp Benjamin Köppchen 209 | Sindre Sorhus 210 | Bernhard Sirlinger 211 | Jared A. Scheel 212 | Rafael Xavier de Souza 213 | John Chen 214 | Robert Beuligmann 215 | Dale Kocian 216 | Mike Sherov 217 | Andrew Couch 218 | Marc-Andre Lafortune 219 | Nate Eagle 220 | David Souther 221 | Mathias Stenbom 222 | Sergey Kartashov 223 | Avinash R 224 | Ethan Romba 225 | Cory Gackenheimer 226 | Juan Pablo Kaniefsky 227 | Roman Salnikov 228 | Anika Henke 229 | Samuel Bovée 230 | Fabrício Matté 231 | Viktor Kojouharov 232 | Pawel Maruszczyk (http://hrabstwo.net) 233 | Pavel Selitskas 234 | Bjørn Johansen 235 | Matthieu Penant 236 | Dominic Barnes 237 | David Sullivan 238 | Thomas Jaggi 239 | Vahid Sohrabloo 240 | Travis Carden 241 | Bruno M. Custódio 242 | Nathanael Silverman 243 | Christian Wenz 244 | Steve Urmston 245 | Zaven Muradyan 246 | Woody Gilk 247 | Zbigniew Motyka 248 | Suhail Alkowaileet 249 | Toshi MARUYAMA 250 | David Hansen 251 | Brian Grinstead 252 | Christian Klammer 253 | Steven Luscher 254 | Gan Eng Chin 255 | Gabriel Schulhof 256 | Alexander Schmitz 257 | Vilhjálmur Skúlason 258 | Siebrand Mazeland 259 | Mohsen Ekhtiari 260 | Pere Orga 261 | Jasper de Groot 262 | Stephane Deschamps 263 | Jyoti Deka 264 | Andrei Picus 265 | Ondrej Novy 266 | Jacob McCutcheon 267 | Monika Piotrowicz 268 | Imants Horsts 269 | Eric Dahl 270 | Dave Stein 271 | Dylan Barrell 272 | Daniel DeGroff 273 | Michael Wiencek 274 | Thomas Meyer 275 | Ruslan Yakhyaev 276 | Brian J. Dowling 277 | Ben Higgins 278 | Yermo Lamers 279 | Patrick Stapleton 280 | Trisha Crowley 281 | Usman Akeju 282 | Rodrigo Menezes 283 | Jacques Perrault 284 | Frederik Elvhage 285 | Will Holley 286 | Uri Gilad 287 | Richard Gibson 288 | Simen Bekkhus 289 | Chen Eshchar 290 | Bruno Pérel 291 | Mohammed Alshehri 292 | Lisa Seacat DeLuca 293 | Anne-Gaelle Colom 294 | Adam Foster 295 | Luke Page 296 | Daniel Owens 297 | Michael Orchard 298 | Marcus Warren 299 | Nils Heuermann 300 | Marco Ziech 301 | Patricia Juarez 302 | Ben Mosher 303 | Ablay Keldibek 304 | Thomas Applencourt 305 | Jiabao Wu 306 | Eric Lee Carraway 307 | Victor Homyakov 308 | Myeongjin Lee 309 | Liran Sharir 310 | Weston Ruter 311 | Mani Mishra 312 | Hannah Methvin 313 | Leonardo Balter 314 | Benjamin Albert 315 | Michał Gołębiowski 316 | Alyosha Pushak 317 | Fahad Ahmad 318 | Matt Brundage 319 | Francesc Baeta 320 | Piotr Baran 321 | Mukul Hase 322 | Konstantin Dinev 323 | Rand Scullard 324 | Dan Strohl 325 | Maksim Ryzhikov 326 | Amine HADDAD 327 | Amanpreet Singh 328 | Alexey Balchunas 329 | Peter Kehl 330 | Peter Dave Hello 331 | Johannes Schäfer 332 | Ville Skyttä 333 | Ryan Oriecuia 334 | -------------------------------------------------------------------------------- /html/3rd/jquery-ui-1.12.1/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright jQuery Foundation and other contributors, https://jquery.org/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery-ui 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | Copyright and related rights for sample code are waived via CC0. Sample 34 | code is defined as all source code contained within the demos directory. 35 | 36 | CC0: http://creativecommons.org/publicdomain/zero/1.0/ 37 | 38 | ==== 39 | 40 | All files located in the node_modules and external directories are 41 | externally maintained libraries used by this software which have their 42 | own licenses; we recommend you read them, as their terms may differ from 43 | the terms above. 44 | -------------------------------------------------------------------------------- /html/3rd/jquery-ui-1.12.1/images/ui-icons_444444_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/3rd/jquery-ui-1.12.1/images/ui-icons_444444_256x240.png -------------------------------------------------------------------------------- /html/3rd/jquery-ui-1.12.1/images/ui-icons_555555_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/3rd/jquery-ui-1.12.1/images/ui-icons_555555_256x240.png -------------------------------------------------------------------------------- /html/3rd/jquery-ui-1.12.1/images/ui-icons_777620_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/3rd/jquery-ui-1.12.1/images/ui-icons_777620_256x240.png -------------------------------------------------------------------------------- /html/3rd/jquery-ui-1.12.1/images/ui-icons_777777_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/3rd/jquery-ui-1.12.1/images/ui-icons_777777_256x240.png -------------------------------------------------------------------------------- /html/3rd/jquery-ui-1.12.1/images/ui-icons_cc0000_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/3rd/jquery-ui-1.12.1/images/ui-icons_cc0000_256x240.png -------------------------------------------------------------------------------- /html/3rd/jquery-ui-1.12.1/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/3rd/jquery-ui-1.12.1/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /html/3rd/jquery-ui-1.12.1/jquery-ui.structure.min.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.12.1 - 2016-09-14 2 | * http://jqueryui.com 3 | * Copyright jQuery Foundation and other contributors; Licensed MIT */ 4 | 5 | .ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;font-size:100%}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-button{padding:.4em 1em;display:inline-block;position:relative;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2em;box-sizing:border-box;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-button-icon-only{text-indent:0}.ui-button-icon-only .ui-icon{position:absolute;top:50%;left:50%;margin-top:-8px;margin-left:-8px}.ui-button.ui-icon-notext .ui-icon{padding:0;width:2.1em;height:2.1em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-icon-notext .ui-icon{width:auto;height:auto;text-indent:0;white-space:normal;padding:.4em 1em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-controlgroup{vertical-align:middle;display:inline-block}.ui-controlgroup > .ui-controlgroup-item{float:left;margin-left:0;margin-right:0}.ui-controlgroup > .ui-controlgroup-item:focus,.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus{z-index:9999}.ui-controlgroup-vertical > .ui-controlgroup-item{display:block;float:none;width:100%;margin-top:0;margin-bottom:0;text-align:left}.ui-controlgroup-vertical .ui-controlgroup-item{box-sizing:border-box}.ui-controlgroup .ui-controlgroup-label{padding:.4em 1em}.ui-controlgroup .ui-controlgroup-label span{font-size:80%}.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item{border-left:none}.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item{border-top:none}.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content{border-right:none}.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content{border-bottom:none}.ui-controlgroup-vertical .ui-spinner-input{width:75%;width:calc( 100% - 2.4em )}.ui-controlgroup-vertical .ui-spinner .ui-spinner-up{border-top-style:solid}.ui-checkboxradio-label .ui-icon-background{box-shadow:inset 1px 1px 1px #ccc;border-radius:.12em;border:none}.ui-checkboxradio-radio-label .ui-icon-background{width:16px;height:16px;border-radius:1em;overflow:visible;border:none}.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon{background-image:none;width:8px;height:8px;border-width:4px;border-style:solid}.ui-checkboxradio-disabled{pointer-events:none}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-n{height:2px;top:0}.ui-dialog .ui-resizable-e{width:2px;right:0}.ui-dialog .ui-resizable-s{height:2px;bottom:0}.ui-dialog .ui-resizable-w{width:2px;left:0}.ui-dialog .ui-resizable-se,.ui-dialog .ui-resizable-sw,.ui-dialog .ui-resizable-ne,.ui-dialog .ui-resizable-nw{width:7px;height:7px}.ui-dialog .ui-resizable-se{right:0;bottom:0}.ui-dialog .ui-resizable-sw{left:0;bottom:0}.ui-dialog .ui-resizable-ne{right:0;top:0}.ui-dialog .ui-resizable-nw{left:0;top:0}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-text{display:block;margin-right:20px;overflow:hidden;text-overflow:ellipsis}.ui-selectmenu-button.ui-button{text-align:left;white-space:nowrap;width:14em}.ui-selectmenu-icon.ui-icon{float:right;margin-top:0}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:.222em 0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:2em}.ui-spinner-button{width:1.6em;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top-style:none;border-bottom-style:none;border-right-style:none}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px}body .ui-tooltip{border-width:2px} -------------------------------------------------------------------------------- /html/3rd/jquery-ui-1.12.1/jquery-ui.theme.min.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.12.1 - 2016-09-14 2 | * http://jqueryui.com 3 | * Copyright jQuery Foundation and other contributors; Licensed MIT */ 4 | 5 | .ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.003;filter:Alpha(Opacity=.3)}.ui-widget-shadow{-webkit-box-shadow:0 0 5px #666;box-shadow:0 0 5px #666} -------------------------------------------------------------------------------- /html/3rd/jquery-ui-1.12.1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-ui", 3 | "title": "jQuery UI", 4 | "description": "A curated set of user interface interactions, effects, widgets, and themes built on top of the jQuery JavaScript Library.", 5 | "version": "1.12.1", 6 | "homepage": "http://jqueryui.com", 7 | "author": { 8 | "name": "jQuery Foundation and other contributors", 9 | "url": "https://github.com/jquery/jquery-ui/blob/1.12.1/AUTHORS.txt" 10 | }, 11 | "main": "ui/widget.js", 12 | "maintainers": [ 13 | { 14 | "name": "Scott González", 15 | "email": "scott.gonzalez@gmail.com", 16 | "url": "http://scottgonzalez.com" 17 | }, 18 | { 19 | "name": "Jörn Zaefferer", 20 | "email": "joern.zaefferer@gmail.com", 21 | "url": "http://bassistance.de" 22 | }, 23 | { 24 | "name": "Mike Sherov", 25 | "email": "mike.sherov@gmail.com", 26 | "url": "http://mike.sherov.com" 27 | }, 28 | { 29 | "name": "TJ VanToll", 30 | "email": "tj.vantoll@gmail.com", 31 | "url": "http://tjvantoll.com" 32 | }, 33 | { 34 | "name": "Felix Nagel", 35 | "email": "info@felixnagel.com", 36 | "url": "http://www.felixnagel.com" 37 | }, 38 | { 39 | "name": "Alex Schmitz", 40 | "email": "arschmitz@gmail.com", 41 | "url": "https://github.com/arschmitz" 42 | } 43 | ], 44 | "repository": { 45 | "type": "git", 46 | "url": "git://github.com/jquery/jquery-ui.git" 47 | }, 48 | "bugs": "https://bugs.jqueryui.com/", 49 | "license": "MIT", 50 | "scripts": { 51 | "test": "grunt" 52 | }, 53 | "dependencies": {}, 54 | "devDependencies": { 55 | "commitplease": "2.3.0", 56 | "grunt": ">=1.3.0", 57 | "grunt-bowercopy": "1.2.4", 58 | "grunt-cli": "0.1.13", 59 | "grunt-compare-size": "0.4.0", 60 | "grunt-contrib-concat": "0.5.1", 61 | "grunt-contrib-csslint": "0.5.0", 62 | "grunt-contrib-jshint": "0.12.0", 63 | "grunt-contrib-qunit": "1.0.1", 64 | "grunt-contrib-requirejs": "0.4.4", 65 | "grunt-contrib-uglify": "0.11.1", 66 | "grunt-git-authors": "3.1.0", 67 | "grunt-html": "6.0.0", 68 | "grunt-jscs": "2.1.0", 69 | "load-grunt-tasks": "3.4.0", 70 | "rimraf": "2.5.1", 71 | "testswarm": "1.1.0" 72 | }, 73 | "keywords": [] 74 | } 75 | -------------------------------------------------------------------------------- /html/extensions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Add the IKKE Extension

5 | 6 | Please complete these steps: 7 |
    8 |
  1. 9 | Install the Ikke extension. 10 |
  2. 11 |
  3. 12 | Finish the installation by clicking here 13 |

    14 |
  4. 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /html/graph.csv: -------------------------------------------------------------------------------- 1 | source,target,value 2 | Harry,Sally,1.2 3 | Harry,Mario,1.3 4 | Sarah,Alice,0.2 5 | Eveie,Alice,0.5 6 | Peter,Alice,1.6 7 | Mario,Alice,0.4 8 | James,Alice,0.6 9 | Harry,Carol,0.7 10 | Harry,Nicky,0.8 11 | Bobby,Frank,0.8 12 | Alice,Mario,0.7 13 | Harry,Lynne,0.5 14 | Sarah,James,1.9 15 | Roger,James,1.1 16 | Maddy,James,0.3 17 | Sonny,Roger,0.5 18 | James,Roger,1.5 19 | Alice,Peter,1.1 20 | Johan,Peter,1.6 21 | Alice,Eveie,0.5 22 | Harry,Eveie,0.1 23 | Eveie,Harry,2.0 24 | Henry,Mikey,0.4 25 | Elric,Mikey,0.6 26 | James,Sarah,1.5 27 | Alice,Sarah,0.6 28 | James,Maddy,0.5 29 | Peter,Johan,0.7 -------------------------------------------------------------------------------- /html/graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "graph": [], 3 | "links": [ 4 | {"source": 0, "target": 1}, 5 | {"source": 0, "target": 2}, 6 | {"source": 0, "target": 3}, 7 | {"source": 0, "target": 4}, 8 | {"source": 0, "target": 5}, 9 | {"source": 0, "target": 6}, 10 | {"source": 1, "target": 3}, 11 | {"source": 1, "target": 4}, 12 | {"source": 1, "target": 5}, 13 | {"source": 1, "target": 6}, 14 | {"source": 2, "target": 4}, 15 | {"source": 2, "target": 5}, 16 | {"source": 2, "target": 6}, 17 | {"source": 3, "target": 5}, 18 | {"source": 3, "target": 6}, 19 | {"source": 5, "target": 6}, 20 | {"source": 0, "target": 7}, 21 | {"source": 1, "target": 8}, 22 | {"source": 2, "target": 9}, 23 | {"source": 3, "target": 10}, 24 | {"source": 4, "target": 11}, 25 | {"source": 5, "target": 12}, 26 | {"source": 6, "target": 13}], 27 | "nodes": [ 28 | {"size": 60, "score": 0, "id": "Androsynth", "type": "circle"}, 29 | {"size": 10, "score": 0.2, "id": "Chenjesu", "type": "circle"}, 30 | {"size": 60, "score": 0.4, "id": "Ilwrath", "type": "circle"}, 31 | {"size": 10, "score": 0.6, "id": "Mycon", "type": "circle"}, 32 | {"size": 60, "score": 0.8, "id": "Spathi", "type": "circle"}, 33 | {"size": 10, "score": 1, "id": "Umgah", "type": "circle"}, 34 | {"id": "VUX", "type": "circle"}, 35 | {"size": 60, "score": 0, "id": "Guardian", "type": "square"}, 36 | {"size": 10, "score": 0.2, "id": "Broodhmome", "type": "square"}, 37 | {"size": 60, "score": 0.4, "id": "Avenger", "type": "square"}, 38 | {"size": 10, "score": 0.6, "id": "Podship", "type": "square"}, 39 | {"size": 60, "score": 0.8, "id": "Eluder", "type": "square"}, 40 | {"size": 10, "score": 1, "id": "Drone", "type": "square"}, 41 | {"id": "Intruder", "type": "square"}], 42 | "directed": false, 43 | "multigraph": false 44 | } -------------------------------------------------------------------------------- /html/icons/blue-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/blue-circle.png -------------------------------------------------------------------------------- /html/icons/browser-web-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/browser-web-icon.png -------------------------------------------------------------------------------- /html/icons/calendar-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/calendar-icon.png -------------------------------------------------------------------------------- /html/icons/delete_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/delete_icon.png -------------------------------------------------------------------------------- /html/icons/excel-xls-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/excel-xls-icon.png -------------------------------------------------------------------------------- /html/icons/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/facebook.png -------------------------------------------------------------------------------- /html/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/favicon.ico -------------------------------------------------------------------------------- /html/icons/file-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/file-icon.png -------------------------------------------------------------------------------- /html/icons/git-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/git-icon.png -------------------------------------------------------------------------------- /html/icons/gmail-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/gmail-icon.png -------------------------------------------------------------------------------- /html/icons/hangouts-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/hangouts-icon.png -------------------------------------------------------------------------------- /html/icons/keynote-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/keynote-icon.png -------------------------------------------------------------------------------- /html/icons/loading_spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/loading_spinner.gif -------------------------------------------------------------------------------- /html/icons/orange-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/orange-circle.png -------------------------------------------------------------------------------- /html/icons/pdf-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/pdf-icon.png -------------------------------------------------------------------------------- /html/icons/person-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/person-icon.png -------------------------------------------------------------------------------- /html/icons/ppt-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/ppt-icon.png -------------------------------------------------------------------------------- /html/icons/rainbow-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/rainbow-circle.png -------------------------------------------------------------------------------- /html/icons/rtf-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/rtf-icon.png -------------------------------------------------------------------------------- /html/icons/sad-computer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/sad-computer.png -------------------------------------------------------------------------------- /html/icons/text-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/text-icon.png -------------------------------------------------------------------------------- /html/icons/tiff-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/tiff-icon.png -------------------------------------------------------------------------------- /html/icons/white_pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/white_pixel.png -------------------------------------------------------------------------------- /html/icons/word-doc-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/html/icons/word-doc-icon.png -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 33 | 78 | 79 |
34 |
35 |
36 | 37 |
38 |
39 |
40 | 41 |
{{account['full_name']}}
42 |
43 |
44 |
45 | 53 | {% for kind in kinds %} 54 |
55 |
56 |
57 |
58 | 59 | 60 |
61 |
62 | 63 |
64 |
65 |
66 | 67 |
68 | Search at Google 69 |
70 |
71 |
72 |
73 |
74 |
75 | {% endfor %} 76 |
77 |
80 |
81 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /html/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 20px; 4 | background: #fafafa; 5 | margin-right: 87px; 6 | } 7 | 8 | 9 | .hidden { 10 | visibility: hidden 11 | } 12 | 13 | text { 14 | font-family: sans-serif; 15 | text-shadow: -2px 0 white, 0 2px white, 2px 0 white, 0 -2px white; 16 | font-size: 1px; 17 | } 18 | 19 | image { 20 | border: 1px solid grey; 21 | } 22 | 23 | .logo { 24 | padding: 9px 15px; 25 | font-family: Arial; 26 | vertical-align: top; 27 | font-weight: bold; 28 | white-space: nowrap; 29 | width: 53px; 30 | font-size: 27px; 31 | margin-top: 32px; 32 | } 33 | 34 | .logo .t1 { 35 | color: #0057e7; 36 | } 37 | 38 | .logo .t2 { 39 | color: #ffa700; 40 | } 41 | 42 | .logo .t3 { 43 | color: #d62d20; 44 | } 45 | 46 | .logo .t4 { 47 | color: #35a853; 48 | } 49 | 50 | #main { 51 | display: none; 52 | width: 100% 53 | } 54 | 55 | svg { 56 | } 57 | 58 | #toolbar { 59 | margin-bottom: 8px; 60 | height: 42px; 61 | } 62 | 63 | #query { 64 | height: 33px; 65 | font-size: 16px; 66 | width: 500px; 67 | border: 0; 68 | padding-left: 12px; 69 | outline: 0; 70 | } 71 | 72 | .querybox { 73 | display: block; 74 | float: left; 75 | padding: 4px; 76 | background-color: #fff; 77 | vertical-align: top; 78 | border-radius: 2px; 79 | box-shadow: 0 2px 2px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08); 80 | transition: box-shadow 200ms cubic-bezier(0.4, 0.0, 0.2, 1); 81 | } 82 | 83 | .profile { 84 | display: block; 85 | float: right; 86 | vertical-align: top; 87 | } 88 | 89 | .profile img { 90 | width: 32px; 91 | margin-top: 7px; 92 | opacity: .9; 93 | border-radius: 50%; 94 | } 95 | 96 | .profile div { 97 | position: absolute; 98 | top: 37px; 99 | right: 162px; 100 | color: #bbb; 101 | font-family: Arial; 102 | font-size: 16px; 103 | height: 18px; 104 | } 105 | 106 | .filter { 107 | font-size: 12px; 108 | font-family: sans-serif; 109 | } 110 | 111 | .filter option { 112 | font-size: 14px; 113 | } 114 | 115 | .filter select { 116 | margin-right: 1000px; 117 | } 118 | 119 | label input { 120 | visibility: hidden; 121 | display:block; 122 | height:0; 123 | width:0; 124 | position:absolute; 125 | overflow:hidden; 126 | } 127 | 128 | label span { 129 | height: 16px; 130 | width: 16px; 131 | border: 1px solid grey; 132 | display: inline-block; 133 | margin-bottom:1px; 134 | } 135 | 136 | [type=checkbox]:checked + span { 137 | background: black; 138 | } 139 | 140 | .tab-contents { 141 | height: 100%; 142 | overflow: auto; 143 | } 144 | 145 | .ui-table th { 146 | display: inline-block; 147 | } 148 | 149 | .ui-table td { 150 | display: inline-block; 151 | text-overflow: ellipsis; 152 | } 153 | 154 | td { 155 | overflow: hidden; 156 | } 157 | 158 | .ui-tabs { 159 | padding: 0; 160 | display: none; 161 | } 162 | .ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor { 163 | color: #4285f4; 164 | } 165 | 166 | .ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { 167 | color: #4285f4; 168 | border-bottom: 3px solid #4285f4; 169 | font-weight: bold; 170 | } 171 | 172 | .ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited, a.ui-button, a:link.ui-button, a:visited.ui-button, .ui-button { 173 | color: #777; 174 | text-decoration: none; 175 | } 176 | 177 | .ui-state-default a:hover { 178 | color: black; 179 | } 180 | 181 | .ui-tabs .ui-tabs-nav .ui-tabs-anchor { 182 | padding: .6em .5em; 183 | } 184 | 185 | .ui-tabs .buttons { 186 | padding: 2px; 187 | } 188 | 189 | .ui-tabs .buttons a { 190 | color: #777; 191 | text-decoration: none; 192 | font-weight: normal; 193 | font-size: 12px; 194 | } 195 | 196 | .ui-tabs .spacer { 197 | margin-left: 30px; 198 | } 199 | 200 | .ui-tabs .buttons .dialog { 201 | padding: 16px; 202 | font-size: 16px; 203 | font-weight: normal; 204 | border-radius: 4px; 205 | box-shadow: 0 4px 4px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08); 206 | transition: box-shadow 200ms cubic-bezier(0.4, 0.0, 0.2, 1); 207 | background: white; 208 | position: absolute; 209 | text-align: left; 210 | display: none; 211 | } 212 | 213 | .dialog span { 214 | margin: 0; 215 | } 216 | 217 | .ui-tabs-nav a { 218 | color: #777; 219 | outline: 0; 220 | padding: 0 8px; 221 | } 222 | 223 | .ui-tabs .ui-tabs-nav { 224 | border: 1px solid #DDD; 225 | border-width: 0 0 1px 0; 226 | background: #fafafa; 227 | } 228 | 229 | .ui-tabs .ui-tabs-nav li { 230 | border: none; 231 | background: #fafafa; 232 | color: #777; 233 | font-size: 13px; 234 | } 235 | 236 | .ui-tabs .ui-tabs-panel { 237 | padding: 0.1em 0.1em; 238 | border: 0px solid #AAA; 239 | height: calc(100% - 100); 240 | } 241 | 242 | .link-header a { 243 | color: #AAA; 244 | } 245 | 246 | .link-header { 247 | color: #AAA; 248 | font-size: 12px; 249 | font-family: Arial; 250 | margin-left: 16px; 251 | } 252 | 253 | .link-header label { 254 | color: black; 255 | margin-left: -2px; 256 | } 257 | 258 | .link { 259 | color: navy; 260 | text-decoration: none; 261 | } 262 | 263 | .link:hover { 264 | text-decoration: underline; 265 | } 266 | 267 | #grid { 268 | position: absolute; 269 | width: 900px; 270 | right: 1px; 271 | margin: 2px; 272 | } 273 | 274 | .view { 275 | opacity: 0; 276 | border: none; 277 | } 278 | 279 | .stats { 280 | font-size: 9px; 281 | margin: 8px; 282 | color: #555; 283 | z-index: 1; 284 | position: absolute; 285 | } 286 | 287 | .summary-link { 288 | display: inline-block; 289 | } 290 | 291 | .summary { 292 | font-size: 14px; 293 | margin: 8px 0 0 4px; 294 | color: #777; 295 | overflow: hidden; 296 | height: 22px; 297 | } 298 | 299 | .summary a { 300 | color: #4285f4; 301 | text-decoration: none; 302 | } 303 | 304 | .summary a:hover { 305 | text-decoration: underline; 306 | } 307 | 308 | #ikke-search-email { 309 | opacity: 0; 310 | } 311 | 312 | .search-button { 313 | -webkit-transform: rotate(45deg); 314 | -moz-transform: rotate(45deg); 315 | -o-transform: rotate(45deg); 316 | transform: rotate(-45deg); 317 | color: #4285f4; 318 | width: 32px; 319 | float: right; 320 | font-weight: bold; 321 | font-size: 24px; 322 | margin-top: -4px; 323 | margin-right: -4px; 324 | cursor: pointer; 325 | } 326 | 327 | .zoom-buttons { 328 | display: none; 329 | position: absolute; 330 | z-index: 200; 331 | padding: 5px; 332 | top: 85px; 333 | } 334 | 335 | .zoom-buttons button { 336 | background-color: white; 337 | border-radius: 2px; 338 | display: block; 339 | width: 29px; 340 | height: 29px; 341 | cursor: pointer; 342 | border: 1px solid #EEE; 343 | margin-top: -1px; 344 | color: #999; 345 | outline: 0; 346 | transition: background-color 0.3s ease-out 347 | } 348 | 349 | table.ui-table { 350 | border: 1px solid #ddd; 351 | font-size: 14px; 352 | } 353 | 354 | table.ui-table th, table.ui-table td { 355 | cursor: pointer; 356 | border-bottom: 1px solid #ddd; 357 | padding: 3px 19px 3px 3px; 358 | text-align: left; 359 | vertical-align: top; 360 | } 361 | 362 | table.ui-table th { 363 | padding-top: 4px; 364 | padding-bottom: 4px; 365 | text-align: left; 366 | background-color: #bababa; 367 | color: white; 368 | } 369 | 370 | table.ui-table tr:nth-child(even) { 371 | background-color: #f2f2f2; 372 | } 373 | 374 | table.ui-table { 375 | border-collapse: collapse; 376 | } 377 | 378 | table.ui-table tr:hover { 379 | background-color: lightblue; 380 | } 381 | 382 | .background { 383 | background: white; 384 | } 385 | 386 | #dashboard { 387 | position: absolute; 388 | top: 0px; 389 | right: 0px; 390 | margin: 4px; 391 | opacity: .3; 392 | } 393 | 394 | #dashboard span { 395 | font-size: 18px; 396 | margin: 0 2px; 397 | } 398 | 399 | .sad { 400 | display: none; 401 | margin: 100px; 402 | width: 500px; 403 | } 404 | 405 | .sad img { 406 | width: 120px; 407 | margin-bottom: 15px; 408 | } 409 | 410 | .sad div { 411 | font-size: 14px; 412 | font-family: Arial; 413 | margin-bottom: 12px; 414 | } 415 | 416 | .sad a { 417 | font-size: 14px; 418 | font-family: Arial; 419 | color: navy; 420 | } 421 | 422 | .spinner { 423 | display: none; 424 | } 425 | 426 | .spinner img { 427 | width: 80px; 428 | } 429 | 430 | #tabs { 431 | border-width: 0; 432 | height: 100%; 433 | display: none; 434 | } 435 | 436 | .searchbox { 437 | } 438 | 439 | .select-wrapper:hover { 440 | border: 0 solid #4285f4; 441 | border-bottom-width: 1px; 442 | } 443 | 444 | select { 445 | -webkit-appearance: none; 446 | border: 0 solid #ddd; 447 | background: transparent; 448 | font-size: 14px; 449 | font-family: Arial; 450 | color: #4285f4; 451 | box-shadow: none; 452 | outline: 0; 453 | } 454 | 455 | #projects { 456 | position: absolute; 457 | right: 10px; 458 | top: 50px; 459 | min-height: 100px; 460 | min-width: 100px; 461 | font-size: 14px; 462 | } -------------------------------------------------------------------------------- /html/projects.js: -------------------------------------------------------------------------------- 1 | // coming soon -------------------------------------------------------------------------------- /html/settings.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-left: 32px; 3 | } 4 | 5 | button { 6 | background: #4CAF50; 7 | border: 1px solid grey; 8 | padding: 5px; 9 | cursor: pointer; 10 | display: inline; 11 | width: 100px; 12 | text-align: center; 13 | font-weight: bold; 14 | height: 24px; 15 | margin: 4px 0; 16 | color: #f2eeee; 17 | border-radius: 3px; 18 | } 19 | 20 | .hidden { 21 | display: none; 22 | } 23 | 24 | button:focus { 25 | outline: none; 26 | } 27 | 28 | .stop-clear-button { 29 | background: orange; 30 | } 31 | 32 | .orange { 33 | background: orange; 34 | } 35 | 36 | .orange:hover { 37 | background: red; 38 | } 39 | 40 | button:hover { 41 | background: #336034; 42 | color: white; 43 | } 44 | 45 | button:disabled { 46 | background: grey; 47 | } 48 | 49 | #ikke-extension { 50 | margin-top: 43px; 51 | display: block; 52 | } 53 | 54 | #ikke-extension button { 55 | width: 150px; 56 | background-color: #ffa700; 57 | } 58 | 59 | #ikke-extension button:hover { 60 | background-color: #dd9000; 61 | } 62 | 63 | table { 64 | margin-top: 15px; 65 | border-collapse: collapse; 66 | } 67 | 68 | tr:nth-child(even) { 69 | background-color: #f2f2f2; 70 | } 71 | 72 | th, td { 73 | text-align: left; 74 | padding: 8px; 75 | font-family: Arial; 76 | } 77 | 78 | th { 79 | background-color: #4CAF50; 80 | color: white; 81 | } 82 | 83 | span,a { 84 | font-size: 12px; 85 | } 86 | 87 | span.name { 88 | font-size: 14px; 89 | text-align: right; 90 | } 91 | 92 | .spinner { 93 | visibility: hidden; 94 | width: 24px; 95 | height: 24px; 96 | } 97 | 98 | .header { 99 | margin-top: 32px; 100 | } 101 | 102 | .logo { 103 | padding: 12px 0; 104 | font-family: Arial; 105 | vertical-align: top; 106 | white-space: nowrap; 107 | font-size: 27px; 108 | cursor: pointer; 109 | display: inline; 110 | } 111 | 112 | .logo .t1 { 113 | font-weight: bold; 114 | color: #0057e7; 115 | font-size: 27px; 116 | } 117 | 118 | .logo .t2 { 119 | font-weight: bold; 120 | color: #ffa700; 121 | font-size: 27px; 122 | } 123 | 124 | .logo .t3 { 125 | font-weight: bold; 126 | color: #d62d20; 127 | font-size: 27px; 128 | } 129 | 130 | .logo .t4 { 131 | font-weight: bold; 132 | color: #35a853; 133 | font-size: 27px; 134 | } 135 | 136 | .title { 137 | margin-left: 26px; 138 | font-family: Arial; 139 | font-size: 25px; 140 | } 141 | 142 | #ikke-settings { 143 | border: 2px solid grey; 144 | border-width: 2px 0 0 0 ; 145 | margin-top: 18px; 146 | padding-top: 15px; 147 | display: none; 148 | } 149 | 150 | .settings-row { 151 | display: flex; 152 | align-items: center; 153 | } 154 | 155 | .settings-row input[type='checkbox'] { 156 | width: 19px; 157 | height: 19px; 158 | } 159 | 160 | .settings-row label { 161 | font-family: Arial; 162 | font-size: 14px; 163 | display: inline; 164 | margin-left: 4px; 165 | margin-top: 2px; 166 | } -------------------------------------------------------------------------------- /html/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
16 | Settings - using {{memory}} 17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 |
26 |
27 |
28 | 29 | 32 |
33 |
34 | 35 | 38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {% for kind in kinds %} 49 | 50 | 53 | 57 | 63 | 66 | 69 | 70 | {% endfor %} 71 |
KindDeleteAddStatus
51 | {{kind.upper()}} 52 | 54 | 55 | 56 | 58 | {% if can_load_more[kind] %} 59 | 60 | 61 | {% endif %} 62 | 64 | calculating... 65 | 67 | 68 |
72 |
73 |
74 | 
75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /html/settings.js: -------------------------------------------------------------------------------- 1 | $('#ikke-gmail-needed').css('display', 'none'); 2 | 3 | function check_history() { 4 | $.getJSON('/status', function(response) { 5 | $('.history').each(function() { 6 | var span = $(this); 7 | var kind = span.closest('tr').attr('kind'); 8 | var load_button = $('#load-button-' + kind); 9 | var stop_load_button = $('#stop-load-button-' + kind); 10 | var clear_button = $('#clear-button-' + kind); 11 | var stop_clear_button = $('#stop-clear-button-' + kind); 12 | var spinner = $('#spinner-' + kind); 13 | var status = response[kind] 14 | var running = status.loading || status.deleting; 15 | span.text(status.history); 16 | spinner.css('visibility', running ? 'visible' : 'hidden'); 17 | load_button.css('display', status.loading ? 'none' : 'block'); 18 | stop_load_button.css('display', status.loading ? 'block' : 'none'); 19 | clear_button.css('display', status.count == 0 || status.deleting ? 'none' : 'block'); 20 | stop_clear_button.css('display', status.count > 0 && status.deleting ? 'block' : 'none'); 21 | }) 22 | setTimeout(check_history, 1000); 23 | }) 24 | .fail(function() { 25 | span.text('No history for ' + kind); 26 | spinner.css('visibility', 'hidden'); 27 | load_button.css('visibility', 'hidden'); 28 | clear_button.css('visibility', 'hidden'); 29 | setTimeout(check_history, 10000); 30 | }) 31 | } 32 | 33 | check_history(); 34 | 35 | $('.logo').click(function() { 36 | document.location = '/'; 37 | }); 38 | 39 | $('.load-button').click(function() { 40 | var kind = $(this).closest('tr').attr('kind'); 41 | $.get('/load?kind=' + kind) 42 | .fail(function(error) { 43 | $('#history-' + kind).text('Could not load more items, ' + error); 44 | } 45 | ); 46 | }); 47 | 48 | $('.stop-load-button').click(function() { 49 | var kind = $(this).closest('tr').attr('kind'); 50 | $.get('/stopload?kind=' + kind) 51 | .fail(function(error) { 52 | $('#history-' + kind).text('Could not stop loading, ' + error); 53 | } 54 | ); 55 | }); 56 | 57 | $('.clear-button').click(function() { 58 | var kind = $(this).closest('tr').attr('kind'); 59 | $.get('/clear?kind=' + kind) 60 | .fail(function(error) { 61 | $('#history-' + kind).text('Could not clear items, ' + error); 62 | } 63 | ); 64 | }); 65 | 66 | $('.stop-clear-button').click(function() { 67 | var kind = $(this).closest('tr').attr('kind'); 68 | $.get('/stopclear?kind=' + kind) 69 | .fail(function(error) { 70 | $('#history-' + kind).text('Could not stop deleting, ' + error); 71 | }); 72 | }); 73 | 74 | $('#settings-debug-browser-extension').on('change', function () { 75 | $.get('/settings_set?key=debug-browser-extension&value=' + $(this).is(":checked")) 76 | .done(function(result) { 77 | console.log(result); 78 | }) 79 | .fail(function(error) { 80 | alert(error); 81 | }); 82 | }); 83 | 84 | $.get('/settings_get?key=debug-browser-extension') 85 | .done(function(result) { 86 | $('#settings-debug-browser-extension').prop('checked', result == "true"); 87 | }) 88 | .fail(function(error) { 89 | alert(error); 90 | }); 91 | 92 | $('#settings-show-ikke-dot').on('change', function () { 93 | console.log("dot?", $(this).is(":checked")); 94 | $.get('/settings_set?key=show-ikke-dot&value=' + $(this).is(":checked")) 95 | .done(function(result) { 96 | console.log("dot?", $(this).is(":checked"), result); 97 | }) 98 | .fail(function(error) { 99 | alert(error); 100 | }); 101 | }); 102 | 103 | $.get('/settings_get?key=show-ikke-dot') 104 | .done(function(result) { 105 | $('#settings-show-ikke-dot').prop('checked', result == "true"); 106 | }) 107 | .fail(function(error) { 108 | alert(error); 109 | }); 110 | 111 | function setup_extension() { 112 | window.open("http://chrislaffra.com/ikke/extension.html", '_blank'); 113 | } 114 | 115 | setInterval(function() { 116 | if ($('#ikke-extension').css('display') == 'block') { 117 | document.location.reload(); 118 | } 119 | }, 30000) 120 | 121 | -------------------------------------------------------------------------------- /htmlparser.py: -------------------------------------------------------------------------------- 1 | from html.parser import HTMLParser 2 | from re import sub 3 | from sys import stderr 4 | from traceback import print_exc 5 | 6 | 7 | class HTMLTextParser(HTMLParser): 8 | def __init__(self): 9 | HTMLParser.__init__(self) 10 | self.__text = [] 11 | self.__skip = False 12 | 13 | def handle_data(self, data): 14 | if not self.__skip: 15 | text = data.strip() 16 | if len(text) > 0: 17 | text = sub('[ \t\r\n]+', ' ', text) 18 | self.__text.append(text + ' ') 19 | 20 | def handle_starttag(self, tag, attrs): 21 | self.__skip = False 22 | if tag == 'p': 23 | self.__text.append('\n\n') 24 | elif tag == 'br': 25 | self.__text.append('\n') 26 | elif tag == 'style': 27 | self.__skip = True 28 | 29 | def handle_startendtag(self, tag, attrs): 30 | if tag == 'br': 31 | self.__text.append('\n\n') 32 | self.__skip = False 33 | 34 | def text(self): 35 | return ''.join(self.__text).strip() 36 | 37 | 38 | import re 39 | REMOVE_STYLE_RE = re.compile('', re.IGNORECASE) 40 | 41 | def get_text(html): 42 | try: 43 | parser = HTMLTextParser() 44 | parser.feed(re.sub(REMOVE_STYLE_RE, '', html)) 45 | parser.close() 46 | return parser.text() 47 | except: 48 | print_exc(file=stderr) 49 | return html 50 | 51 | 52 | def main(): 53 | text = r''' 54 | 55 | 56 | Project: DeHTML
57 | Description:
58 | This small script is intended to allow conversion from HTML markup to 59 | plain text. 60 | 61 | 62 | ''' 63 | print(get_text(text)) 64 | 65 | 66 | if __name__ == '__main__': 67 | main() -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/images/architecture.png -------------------------------------------------------------------------------- /images/screenshot-context-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/images/screenshot-context-menu.png -------------------------------------------------------------------------------- /images/screenshot-grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/images/screenshot-grid.png -------------------------------------------------------------------------------- /images/screenshot-ikke-dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/images/screenshot-ikke-dot.png -------------------------------------------------------------------------------- /images/screenshot-ikke-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/images/screenshot-ikke-graph.png -------------------------------------------------------------------------------- /images/screenshot-ikke-related.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/images/screenshot-ikke-related.png -------------------------------------------------------------------------------- /images/screenshot-ikke-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/images/screenshot-ikke-settings.png -------------------------------------------------------------------------------- /images/screenshot-ikke-statusbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laffra/Ikke/5c0e0a4103e66de53469233c7c4a07995ad7d3d8/images/screenshot-ikke-statusbar.png -------------------------------------------------------------------------------- /importers/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from importers import contact 3 | from settings import settings 4 | import datetime 5 | import logging 6 | import storage 7 | import sys 8 | import pynsights 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | MAXIMUM_DAYS_LOAD = 20 * 365 13 | INITIAL_DAYS_LOAD = 365 14 | DAYS_LOAD = 31 15 | DAYS_TO_LOOK_INTO_THE_FUTURE = 14 16 | 17 | class Importer(): 18 | def __init__(self): 19 | self.kind = "" 20 | 21 | 22 | def update_days(self, days): 23 | self.update_timestamp((datetime.datetime.utcnow() - datetime.timedelta(days)).timestamp()) 24 | 25 | 26 | def update_timestamp(self, timestamp): 27 | key_before = '%s/timestamp_before' % self.kind 28 | settings[key_before] = max(timestamp, settings.get(key_before, 0)) 29 | key_after = '%s/timestamp_after' % self.kind 30 | settings[key_after] = min(timestamp, settings.get(key_after, sys.maxsize)) 31 | 32 | 33 | @abstractmethod 34 | def load_items(self, days, start): 35 | pass 36 | 37 | @pynsights.trace 38 | def load(self): 39 | try: 40 | settings['%s/loading' % self.kind] = True 41 | 42 | # load recent items added since we last checked 43 | self.load_items_before(self.get_days(datetime.datetime.utcnow().timestamp()) - DAYS_TO_LOOK_INTO_THE_FUTURE) 44 | 45 | # load olders items and fill up the index 46 | days_after = self.get_days(settings['%s/timestamp_after' % self.kind]) + 1 47 | if days_after < MAXIMUM_DAYS_LOAD: 48 | self.load_items_before(days_after) 49 | finally: 50 | settings['%s/loading' % self.kind] = False 51 | contact.cleanup() 52 | storage.Storage.log_search_stats() 53 | 54 | def get_days(self, timestamp): 55 | delta = datetime.datetime.utcnow() - datetime.datetime.fromtimestamp(timestamp) 56 | return delta.days 57 | 58 | def load_items_before(self, days_before): 59 | days = DAYS_LOAD if '%s/count' % self.kind in settings else INITIAL_DAYS_LOAD 60 | last_day_before = self.get_days(settings.get('%s/timestamp_before' % self.kind, datetime.datetime.utcnow().timestamp())) 61 | days_after = min(last_day_before, days_before + days) if days_before <= last_day_before else days_before + days 62 | days_after = max(1, days_after) 63 | pynsights.annotate("[%s %d/%d]" % ( 64 | self.__class__.__name__, 65 | days_after, 66 | days_before 67 | )) 68 | self.load_items(days_after, days_before) 69 | 70 | @classmethod 71 | def get_status(cls, kind, label): 72 | count = settings['%s/count' % kind] 73 | before = datetime.datetime.fromtimestamp(settings['%s/timestamp_before' % kind]).date() 74 | after = datetime.datetime.fromtimestamp(settings['%s/timestamp_after' % kind]).date() 75 | details = ' - [%s - %s]' % (after, before) if count else "" 76 | return '%d %s%s %s' % (count, label, "" if count == 1 else "s", details) 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /importers/browser.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from importers import Importer 3 | import json 4 | import logging 5 | import os 6 | from os.path import expanduser 7 | import re 8 | from settings import settings 9 | import shutil 10 | import sqlite3 11 | from storage import Storage 12 | import sys 13 | import time 14 | import utils 15 | from urllib.parse import urlparse 16 | import stopwords 17 | import storage 18 | from threadpool import ThreadPool 19 | 20 | HISTORY_QUERY_URLS = 'select visit_count, last_visit_time, title, url from urls' 21 | 22 | META_DOMAINS = { 23 | '//www.google.nl', 24 | '//www.google.com', 25 | '//mail.google.com', 26 | '//maps.google.com', 27 | '//maps.google.com', 28 | '//ad.doubleclick.net', 29 | '//rfihub.com', 30 | '//adnxs.com', 31 | '//photos.google.com/search', 32 | '//linkedin.com/search', 33 | '//search.ikke.io', 34 | '//file:', 35 | '//localhost:', 36 | '//127.0.0.1:', 37 | } 38 | META_DOMAINS_RE = re.compile('|'.join(META_DOMAINS)) 39 | CLEANUP_URL_PATH_RE = re.compile('\W+') 40 | MAX_FILENAME_LENGTH = 76 41 | MAX_FILENAME_FRACTION = 35 42 | 43 | chrome_epoch = datetime.datetime(1601,1,1) 44 | is_loading_items = False 45 | logger = logging.getLogger(__name__) 46 | 47 | 48 | def get_history_path(): 49 | if os.name == 'nt': 50 | return ['AppData', 'Local', 'Google', 'Chrome', 'User Data', 'Default', 'history'] 51 | else: 52 | return ['Library', 'Application Support', 'Google', 'Chrome', 'Default', 'History'] 53 | 54 | def is_meta_site(url): 55 | return META_DOMAINS_RE.search(url) 56 | 57 | 58 | def adjust_chrome_timestamp(chrome_timestamp): 59 | delta = datetime.timedelta(microseconds=int(chrome_timestamp)) 60 | return utils.get_timestamp(chrome_epoch + delta) 61 | 62 | 63 | def process_url(rows): 64 | if rows: 65 | for n,row in enumerate(rows): 66 | if not settings['browser/loading']: 67 | break 68 | visit_count, last_visit_time, title, url = row 69 | if n % 1000 == 0: 70 | logger.debug('Add %s %s' % (title, url)) 71 | if title: 72 | save_image(url, title, '', get_favicon(url), '', adjust_chrome_timestamp(last_visit_time), keywords=[]) 73 | 74 | 75 | def get_favicon(url): 76 | url = urlparse(url) 77 | return '%s://%s/favicon.ico' % (url.scheme, url.netloc) 78 | 79 | 80 | def save_image(url, title, image, favicon, selection, essence, keywords, timestamp=0): 81 | if is_meta_site(url): 82 | return 83 | domain = urlparse(url).netloc 84 | timestamp = timestamp or utils.get_timestamp() 85 | title = ' '.join(stopwords.remove_stopwords(title)) 86 | uid = '#'.join([domain, title]) 87 | data = Storage.get_data("browser", uid) 88 | selection = "%s %s" % (selection, data.get("selection", "") if data else "") 89 | image = image or data.get("image", image) if data else "" 90 | selection = ' '.join(stopwords.remove_stopwords(selection)) 91 | settings.increment('browser/added') 92 | settings.increment('browser/count') 93 | logger.info("Save browser image %s image=%s timestamp=%s selection=%s" % (url, image, timestamp, selection)) 94 | Storage.add_data({ 95 | 'kind': 'browser', 96 | 'uid': uid, 97 | 'url': url, 98 | 'domain': domain, 99 | 'label': domain, 100 | 'image': image, 101 | 'icon': favicon, 102 | 'selection': selection, 103 | 'essence': essence, 104 | 'keywords': keywords, 105 | 'title': title, 106 | 'timestamp': timestamp, 107 | }) 108 | update_timestamp(timestamp) 109 | 110 | 111 | def update_timestamp(timestamp): 112 | settings['browser/timestamp_before'] = max(timestamp, settings.get('browser/timestamp_before', 0)) 113 | settings['browser/timestamp_after'] = min(timestamp, settings.get('browser/timestamp_after', sys.maxsize)) 114 | 115 | 116 | def load_history(): 117 | home = expanduser("~") 118 | history_path = os.path.join(home, *get_history_path()) 119 | copy_path = history_path + '_copy' 120 | shutil.copy(history_path, copy_path) 121 | connection = sqlite3.connect(copy_path) 122 | cursor = connection.cursor() 123 | query = HISTORY_QUERY_URLS 124 | cursor.execute(query) 125 | thread_count = 64 126 | pool = ThreadPool(thread_count) 127 | logger.info('Loading browser history') 128 | settings['browser/added'] = 0 129 | settings['browser/when'] = time.time() 130 | seen = set() 131 | rows = [] 132 | for row in cursor.fetchall(): 133 | url = row[-1] 134 | if not is_meta_site(url) and not url in seen: 135 | rows.append(row) 136 | seen.add(url) 137 | chunk_size = 2 * int(len(rows) / thread_count) 138 | for n in range(thread_count + 1): 139 | start, end = n*chunk_size, (n+1)*chunk_size 140 | pool.add_task(process_url, rows[start: end]) 141 | count = settings['browser/added'] 142 | pool.wait_completion() 143 | logger.info('%d urls added with %d threads with chunksize %d.' % (count, thread_count, chunk_size)) 144 | 145 | 146 | def get_status(): 147 | return Importer.get_status("browser", "site") 148 | 149 | 150 | def delete_all(): 151 | return storage.Storage.clear('browser') 152 | 153 | 154 | def load(): 155 | global is_loading_items 156 | is_loading_items = True 157 | try: 158 | load_history() 159 | finally: 160 | is_loading_items = False 161 | 162 | 163 | def stop_loading(): 164 | global is_loading_items 165 | is_loading_items = False 166 | 167 | 168 | def is_loading(): 169 | return is_loading_items 170 | 171 | 172 | class BrowserNode(storage.Data): 173 | def __init__(self, obj): 174 | super(BrowserNode, self).__init__(obj.get('label','???'), obj) 175 | self.kind = 'browser' 176 | self.uid = obj['uid'] 177 | self.image = obj.get('image','') 178 | self.essence = obj.get('essence','') 179 | self.keywords = obj.get('keywords','') 180 | self.url = obj.get('url', '') 181 | self.domain, self.title = obj['uid'].split('#') 182 | self.timestamp = float(obj.get('timestamp', '0')) 183 | self.label = self.domain 184 | self.selection = obj.get('selection', '') 185 | words = (self.selection + ' ' + self.title).split(' ') 186 | self.words = list(set(word.lower() for word in words)) 187 | self.color = 'navy' 188 | self.icon = obj['icon'] if ".google.com" in self.url else self.image or obj["icon"] 189 | self.icon_size = 24 if ".google.com" in self.url else 48 if self.image else 24 190 | self.font_size = 12 191 | self.zoomed_icon_size = 182 192 | self.node_size = 1 193 | dict.update(self, vars(self)) 194 | 195 | @classmethod 196 | def deserialize(cls, obj): 197 | return BrowserNode(obj) 198 | 199 | def update(self, obj): 200 | super(BrowserNode, self).update(obj) 201 | if self.selection: 202 | obj['selection'] = '%s %s' % (obj.get('selection', ''), self.selection) 203 | obj['title'] = obj.get('title', self.title) 204 | obj['image'] = self.image or obj['image'] 205 | if self.words: 206 | words = list(set(self.words + obj['words'])) 207 | obj['words'] = ' '.join(stopwords.remove_stopwords(' '.join(words))) 208 | 209 | def is_related_item(self, other): 210 | return other.kind == 'browser' and self.domain == other.domain 211 | 212 | def __eq__(self, other): 213 | return other.kind == 'browser' and self.domain == other.domain and self.timestamp == other.timestamp and self.title == other.title 214 | 215 | def __hash__(self): 216 | return hash("%s-%s" % (self.domain, self.title)) 217 | 218 | def is_duplicate(self, duplicates): 219 | if is_meta_site(self.url) or self.domain in duplicates: 220 | self.mark_duplicate() 221 | return True 222 | duplicates.add(self.domain) 223 | return False 224 | 225 | 226 | def render(args): 227 | return '' % args.get("url", json.dumps(args)) 228 | 229 | 230 | def cleanup(): 231 | pass 232 | 233 | 234 | def poll(): 235 | pass 236 | 237 | 238 | settings['browser/can_load_more'] = True 239 | settings['browser/can_delete'] = True 240 | 241 | deserialize = BrowserNode.deserialize 242 | 243 | if __name__ == '__main__': 244 | logging.basicConfig(level=logging.INFO) 245 | load_history() 246 | for n,obj in enumerate(Storage.search('reddit', days=100000)): 247 | logger.info('Result %d: %s', n, json.dumps(obj, indent=4)) 248 | 249 | -------------------------------------------------------------------------------- /importers/calendar.py: -------------------------------------------------------------------------------- 1 | from importers import contact 2 | from importers import google_apis 3 | from importers import Importer 4 | from settings import settings 5 | 6 | import datetime 7 | import dateparser 8 | import logging 9 | import stopwords 10 | import storage 11 | 12 | logger = logging.getLogger(__name__) 13 | service = google_apis.get_google_service("calendar", "v3") 14 | 15 | 16 | class Calendar(Importer): 17 | singleton = None 18 | 19 | def __init__(self): 20 | super().__init__() 21 | self.kind = "calendar" 22 | self.error = None 23 | Calendar.singleton = self 24 | 25 | @classmethod 26 | def get_body(cls, event): 27 | return event 28 | 29 | def load_items(self, days_after, days_before): 30 | day = days_before 31 | while settings['calendar/loading'] and day < days_after: 32 | try: 33 | self.update_days(day) 34 | before = datetime.datetime.utcnow() + datetime.timedelta(days=1) - datetime.timedelta(days=day) 35 | after = before - datetime.timedelta(days=2) 36 | result = service.events().list( 37 | calendarId='primary', 38 | timeMin=after.isoformat() + "Z", 39 | timeMax=before.isoformat() + "Z" 40 | ).execute() 41 | logger.info('Loading calendar events before %s after %s => %d items' % (before, after, len(result.get('items',[])))) 42 | self.parse_events(result.get('items', [])) 43 | except Exception as e: 44 | logger.error('Cannot load message for day %d: %s' % (day, e)) 45 | import traceback 46 | traceback.print_exc() 47 | return 48 | else: 49 | day += 1 50 | 51 | def get_names(self, event): 52 | return list(set([attendee["email"] for attendee in event.get("attendees", [])] + [event["organizer"]["email"]])) 53 | 54 | def parse_events(self, events): 55 | for event in events: 56 | import json 57 | start = event.get("start") 58 | if not start or not "dateTime" in start: 59 | continue 60 | when = dateparser.parse(start["dateTime"]) 61 | timestamp = when.timestamp() 62 | names = self.get_names(event) 63 | for email_address in names: 64 | contact.find_contact(email_address.lower(), "", timestamp=timestamp) 65 | storage.Storage.add_data({ 66 | "kind": "calendar", 67 | "uid": event["id"], 68 | "label": event.get("summary", ""), 69 | "description": event.get("description", ""), 70 | "words": stopwords.remove_stopwords("%s %s" % (event.get("summary", ""), event.get("description", ""))), 71 | "names": names, 72 | "url": event["htmlLink"], 73 | "start": start["dateTime"], 74 | "end": event["end"]["dateTime"], 75 | "timestamp": timestamp, 76 | "hangout": event.get("hangoutLink", "") 77 | }) 78 | settings['calendar/count'] += 1 79 | contact.cleanup() 80 | 81 | @classmethod 82 | def get_status(cls): 83 | return Importer.get_status("calendar", "event") 84 | 85 | 86 | class CalendarNode(storage.Data): 87 | def __init__(self, obj): 88 | super(CalendarNode, self).__init__(obj.get('label', '')) 89 | self.kind = 'calendar' 90 | self.uid = obj['uid'] 91 | self.description = obj.get('message_id', '') 92 | self.color = 'blue' 93 | self.timestamp = obj.get('timestamp') 94 | self.icon = 'get?path=icons/calendar-icon.png' 95 | self.icon_size = 34 96 | self.font_size = 10 97 | self.zoomed_icon_size = 52 98 | self.label = '%s %s' % (obj['start'].split('T')[0], obj["label"]) 99 | self.description = obj["description"] 100 | self.words = obj["words"] 101 | self.names = obj["names"] 102 | self.start = obj["start"] 103 | self.end = obj["end"] 104 | self.url = obj["url"] 105 | self.hangout = obj["hangout"] 106 | dict.update(self, vars(self)) 107 | 108 | def __hash__(self): 109 | return hash(self.uid) 110 | 111 | def is_related_item(self, other): 112 | if other.kind != "contact": 113 | return False 114 | for name in self.names: 115 | if name == other.email: 116 | return True 117 | return False 118 | 119 | def is_duplicate(self, duplicates): 120 | key = "calendar - %s" % ' '.join(sorted(word for word in self.words if not stopwords.is_stopword(word))) 121 | if key in duplicates: 122 | self.mark_duplicate() 123 | return True 124 | duplicates.add(key) 125 | return False 126 | 127 | @classmethod 128 | def deserialize(cls, obj): 129 | try: 130 | return CalendarNode(obj) 131 | except Exception as e: 132 | logger.error('Cannot deserialize:' + e) 133 | for k,v in obj.items(): 134 | logger.error('%s: %s' % (k, v)) 135 | raise 136 | 137 | 138 | def render(args): 139 | return "

Open in Calendar" % args.get("url", "calendar.google.com") 140 | 141 | 142 | def delete_all(): 143 | pass 144 | 145 | 146 | def load(): 147 | Calendar.singleton.load() 148 | 149 | 150 | def poll(): 151 | load() 152 | 153 | 154 | def deserialize(obj): 155 | return CalendarNode(obj) 156 | 157 | 158 | settings['calendar/can_load_more'] = True 159 | settings['calendar/pending'] = True 160 | get_status = Calendar.get_status 161 | 162 | Calendar.singleton = Calendar() 163 | 164 | 165 | def cleanup(): 166 | pass 167 | 168 | if __name__ == '__main__': 169 | Calendar.singleton.load() 170 | 171 | -------------------------------------------------------------------------------- /importers/contact.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import logging 3 | import quopri 4 | import re 5 | from settings import settings 6 | import stopwords 7 | import storage 8 | import time 9 | from urllib.parse import quote 10 | 11 | 12 | NAME_CLEANUP_RE = re.compile('\'') 13 | 14 | contacts_cache = {} 15 | save_queue = set() 16 | 17 | 18 | def decode(encoded_string): 19 | encoded_word_regex = r'=\?{1}(.+)\?{1}([B|Q])\?{1}(.+)\?{1}=' 20 | match = re.match(encoded_word_regex, encoded_string) 21 | if match: 22 | charset, encoding, encoded_text = match.groups() 23 | byte_string = base64.b64decode(encoded_text) if encoding == 'B' else quopri.decodestring(encoded_text) 24 | return byte_string.decode(charset) 25 | return encoded_string 26 | 27 | 28 | def remove_quotes(name): 29 | return ' '.join(re.sub(NAME_CLEANUP_RE, '', decode(name)).split(', ')) 30 | 31 | 32 | keys = ('uid', 'email', 'label', 'names', 'phones', 'timestamp') 33 | path_keys = ('label', 'uid', 'icon', 'image', 'words') 34 | 35 | def find_contact(email, name='', phones=None, timestamp=None): 36 | assert email, 'Email missing' 37 | name = remove_quotes(name) 38 | email = remove_quotes(email) 39 | contact = contacts_cache.get(email) 40 | if not contact: 41 | contact = Contact({ 42 | 'kind': 'contact', 43 | 'uid': email, 44 | 'email': email, 45 | 'label': name, 46 | 'words': stopwords.remove_stopwords(name), 47 | 'names': [name] if name else [], 48 | 'timestamp': timestamp, 49 | 'phones': phones or [], 50 | }) 51 | logging.debug('CONTACT: new contact ==> %s %s' % (email, contact.names)) 52 | save_queue.add(contact) 53 | if name and not name in contact.names: 54 | contact.names.append(name) 55 | save_queue.add(contact) 56 | contacts_cache[email] = contact 57 | return contact 58 | 59 | 60 | class Contact(storage.Data): 61 | def __init__(self, obj): 62 | super(Contact, self).__init__(obj.get('email') or obj.get('label'), obj) 63 | self.kind = 'contact' 64 | self.uid = obj['uid'] or obj['email'] 65 | self.email = obj.get('email') 66 | self.names = obj.get('names', []) 67 | self.phones = obj.get('phones',[]) 68 | if len(self.names) > 1: 69 | import collections 70 | counter = collections.Counter() 71 | counter.update(' '.join(self.names).split(' ')) 72 | self.name = '%s %s' % (counter.most_common(1)[0][0], self.email.split('@')[1]) 73 | else: 74 | self.name = self.names and self.names[0] or self.email 75 | self.name = self.name or self.label or self.email 76 | self.label = self.name 77 | self.color = 'purple' 78 | self.icon = 'get?path=icons/person-icon.png' 79 | self.font_size = 14 80 | self.timestamp = obj.get('timestamp', 0) 81 | dict.update(self, vars(self)) 82 | 83 | @classmethod 84 | def deserialize(cls, obj): 85 | # type (dict) -> dict 86 | return Contact(obj) 87 | 88 | def update(self, obj): 89 | super(Contact, self).update(obj) 90 | obj['email'] = obj['email'] or self.email 91 | obj['names'] = list(set(obj['names'] + self.names)) 92 | obj['phones'] = list(set(obj['phones'] + self.phones)) 93 | 94 | def is_duplicate(self, duplicates): 95 | if self.email in duplicates: 96 | self.mark_duplicate() 97 | return True 98 | duplicates.add(self.email) 99 | return False 100 | 101 | def __hash__(self): 102 | return hash(self.uid) 103 | 104 | def __eq__(self, other): 105 | return isinstance(other, self.__class__) and self.uid == other.uid 106 | 107 | def render(self, query): 108 | url = '/?q=%s' % quote(self.label) 109 | return 'Search in Ikke' % url 110 | 111 | def __repr__(self): 112 | return "" % self.email 113 | 114 | 115 | 116 | deserialize = Contact.deserialize 117 | 118 | 119 | def delete_all(): 120 | storage.Storage.clear('contact') 121 | settings['contact/count'] = 0 122 | 123 | 124 | def can_load_more(): 125 | return False 126 | 127 | 128 | def poll(): 129 | pass 130 | 131 | 132 | def get_status(): 133 | count = settings['contact/count'] 134 | return '%d contacts' % count 135 | 136 | 137 | def is_loading(): 138 | return False 139 | 140 | 141 | def stop_loading(): 142 | pass 143 | 144 | 145 | def cleanup(): 146 | if save_queue: 147 | logging.debug('CONTACT: cleanup, save %d contacts' % len(save_queue)) 148 | for _, contact in enumerate(save_queue.copy()): 149 | settings.increment('contact/count') 150 | contact.save() 151 | save_queue.clear() 152 | 153 | 154 | def render(args): 155 | return "" 156 | 157 | if __name__ == '__main__': 158 | import os 159 | import utils 160 | for _,_,files in os.walk(utils.CONTACT_DIR): 161 | for n,file in enumerate(files): 162 | item = storage.Storage.resolve_path(os.path.join(utils.CONTACT_DIR, file)) 163 | item.words = stopwords.remove_stopwords(item.name) 164 | if n%100==0: print(n, item.words) 165 | #test() 166 | -------------------------------------------------------------------------------- /importers/download.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import logging 4 | from settings import settings 5 | import storage 6 | import time 7 | 8 | 9 | DOWNLOADS_DIR = os.path.join(os.path.expanduser('~'), 'Downloads') 10 | 11 | SKIP_CONTENT = { 12 | '.zip', '.dmg', '.exe', '.html', '.htm', 13 | } 14 | 15 | ONE_WEEK_SECONDS = 7 * 24 * 60 * 60 16 | 17 | 18 | class Download: 19 | @classmethod 20 | def load(cls): 21 | last_check = settings['download/xlastcheck'] 22 | now = time.time() 23 | try: 24 | for path, dirs, files in os.walk(DOWNLOADS_DIR): 25 | for filename in filter(lambda f: f[0] != '.', files): 26 | if not settings['download/loading']: 27 | return 28 | name, extension = os.path.splitext(filename) 29 | file_path = os.path.join(path, filename) 30 | timestamp = os.path.getctime(file_path) 31 | if timestamp > last_check: 32 | if extension.lower() in SKIP_CONTENT: 33 | logging.info('skip %s' % filename) 34 | continue 35 | with open(file_path, 'rb') as fin: 36 | settings.increment('file/count') 37 | storage.Storage.add_binary_data(fin.read(), { 38 | 'uid': filename, 39 | 'kind': 'file', 40 | 'timestamp': timestamp, 41 | }) 42 | settings.increment('download/count') 43 | if now - timestamp > ONE_WEEK_SECONDS: 44 | logging.debug('DOWNLOAD: discard', filename) 45 | # os.remove(file_path) 46 | finally: 47 | settings['download/lastcheck'] = now 48 | 49 | @classmethod 50 | def history(cls): 51 | timestamp = settings.get('download/xlastcheck', time.time()) 52 | return datetime.datetime.fromtimestamp(timestamp).date() 53 | 54 | 55 | def can_load_more(): 56 | return True 57 | 58 | 59 | load = Download.load 60 | poll = Download.load 61 | history = Download.history 62 | 63 | 64 | def cleanup(): 65 | pass 66 | 67 | 68 | if __name__ == '__main__': 69 | poll() 70 | -------------------------------------------------------------------------------- /importers/file.py: -------------------------------------------------------------------------------- 1 | from genericpath import getsize 2 | import json 3 | import logging 4 | import os 5 | from settings import settings 6 | import stopwords 7 | import storage 8 | import urllib 9 | import utils 10 | 11 | 12 | keys = ('uid', 'timestamp') 13 | path_keys = ('label', 'uid', 'icon', 'image', 'words') 14 | logger = logging.getLogger(__name__) 15 | 16 | FILE_FORMAT_ICONS = { 17 | 'pdf': 'icons/pdf-icon.png', 18 | 'rtf': 'icons/rtf-icon.png', 19 | 'doc': 'icons/word-doc-icon.png', 20 | 'docx': 'icons/word-doc-icon.png', 21 | 'ics': 'icons/calendar-icon.png', 22 | 'xls': 'icons/excel-xls-icon.png', 23 | 'xlsx': 'icons/excel-xls-icon.png', 24 | 'pages': 'icons/keynote-icon.png', 25 | 'ppt': 'icons/ppt-icon.png', 26 | 'ico': 'icons/ico.png', 27 | 'tiff': 'icons/tiff-icon.png', 28 | 'pptx': 'icons/ppt-icon.png', 29 | 'www': 'icons/browser-web-icon.png', 30 | 'txt': 'icons/text-icon.png', 31 | 'file': 'icons/file-icon.png', 32 | } 33 | FILE_FORMAT_IMAGE_EXTENSIONS = { 'png', 'ico', 'jpg', 'jpeg', 'gif', 'pnm' } 34 | 35 | def deserialize(obj): 36 | logger.info(json.dumps(obj, indent=4)) 37 | return FileItem(obj['path']) 38 | 39 | 40 | def get_status(): 41 | return '%d files' % settings['file/count'] 42 | 43 | 44 | def delete_all(): 45 | pass 46 | 47 | 48 | def poll(): 49 | pass 50 | 51 | 52 | def can_load_more(): 53 | return False 54 | 55 | 56 | def load(): 57 | pass 58 | 59 | 60 | def save_file(uid, filename, timestamp, data): 61 | logger.debug('create_file %s - %s - %s - %d bytes' % (uid, filename, timestamp, len(data))) 62 | save_metadata(uid, filename, timestamp, data) 63 | save_binary_data(uid, filename, timestamp, data) 64 | 65 | 66 | def save_metadata(uid, filename, timestamp, data): 67 | metadata = { 68 | 'kind': 'file', 69 | 'uid': "%s/%s" % (uid, filename), 70 | 'filename': filename, 71 | 'label': filename, 72 | 'words': stopwords.remove_stopwords(filename.replace('+', ' ')), 73 | 'label': filename, 74 | 'timestamp': timestamp or utils.get_timestamp() 75 | } 76 | storage.Storage.add_data(metadata) 77 | write(os.path.join(utils.FILE_DIR, uid, "%s.json" % filename), "w", json.dumps(metadata, indent=4)) 78 | settings.increment('file/count') 79 | 80 | 81 | def load_file(uid, filename): 82 | with open(os.path.join(utils.FILE_DIR, uid, "%s.json" % filename)) as fin: 83 | return FileItem(json.load(fin)) 84 | 85 | 86 | def get_icon(path): 87 | extension = os.path.splitext(path)[1][1:].lower() 88 | icon = FILE_FORMAT_ICONS.get(extension) 89 | route = 'get' if icon else 'get_image' 90 | return '%s?path=%s' % (route, icon or urllib.parse.quote(path)) 91 | 92 | 93 | def save_binary_data(uid, filename, timestamp, data): 94 | write(os.path.join(utils.FILE_DIR, uid, filename), "wb", data) 95 | 96 | 97 | def write(path, format, data): 98 | dir = os.path.dirname(path) 99 | if not os.path.exists(dir): 100 | os.makedirs(os.path.dirname(path)) 101 | with open(path, format) as fout: 102 | fout.write(data) 103 | 104 | 105 | def render(args): 106 | path = os.path.join(utils.FILE_DIR, args["uid"]) 107 | return ''' 108 | 114 | ''' % urllib.parse.quote(path) 115 | 116 | 117 | 118 | class FileItem(storage.Data): 119 | def __init__(self, obj): 120 | super(FileItem, self).__init__(obj['label']) 121 | self.kind = 'file' 122 | self.color = 'blue' 123 | self.filename = obj['filename'] 124 | self.uid = obj['uid'] 125 | if not "/" in self.uid: 126 | self.uid = "%s/%s" % (self.uid, self.filename) 127 | self.label = obj['label'] 128 | self.timestamp = obj['timestamp'] 129 | self.words = obj['words'] 130 | self.path = self.uid 131 | self.icon = get_icon(self.path) 132 | self.icon_size = 44 133 | self.zoomed_icon_size = 512 134 | dict.update(self, vars(self)) 135 | 136 | def is_related_item(self, other): 137 | return False 138 | 139 | def get_key(self): 140 | path = os.path.join(utils.FILE_DIR, self.path) 141 | return 'file-%s-%s' % (self.label, os.path.getsize(path)) 142 | 143 | def is_duplicate(self, duplicates): 144 | key = self.get_key() 145 | if key in duplicates: 146 | self.mark_duplicate() 147 | return True 148 | duplicates.add(key) 149 | 150 | def __eq__(self, other): 151 | return self.kind == other.kind and self.get_key() == other.get_key() 152 | 153 | def __hash__(self): 154 | return hash(self.path) 155 | 156 | def __repr__(self): 157 | return ''.format(self.path) 158 | -------------------------------------------------------------------------------- /importers/git.py: -------------------------------------------------------------------------------- 1 | from classify import Label 2 | import json 3 | import pydriller 4 | from settings import settings 5 | import storage 6 | import logging 7 | from threadpool import ThreadPool 8 | 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | def deserialize(obj): 14 | return obj if isinstance(obj, storage.File) else storage.File(obj['path']) 15 | 16 | 17 | def get_status(): 18 | return '%d commits' % settings['git/count'] 19 | 20 | 21 | def delete_all(): 22 | pass 23 | 24 | 25 | def poll(): 26 | load() 27 | 28 | 29 | def can_load_more(): 30 | return False 31 | 32 | 33 | def load(): 34 | settings["git/loading"] = True 35 | try: 36 | paths = settings["git/paths"] 37 | pool = ThreadPool(len(paths)) 38 | for path in paths: 39 | pool.add_task(load_repo, path) 40 | pool.wait_completion() 41 | finally: 42 | settings["git/loading"] = False 43 | 44 | COLORS = [ 45 | "rgb(0,107,164)", 46 | "rgb(255,128,14)", 47 | "rgb(171,171,171)", 48 | "rgb(89,89,89)", 49 | "rgb(95,158,209)", 50 | "rgb(200,82,0)", 51 | "rgb(137,137,137)", 52 | "rgb(163,200,236)", 53 | "rgb(255,188,121)", 54 | "rgb(34,34,34)", 55 | ] 56 | 57 | class Project(Label): 58 | def __init__(self, name): 59 | super(Project, self).__init__(name) 60 | self.color = COLORS[ hash(name) % len(COLORS) ] 61 | self.font_size = 24 62 | 63 | 64 | class GitCommit(storage.Data): 65 | def __init__(self, obj): 66 | super(GitCommit, self).__init__(obj) 67 | self.uid = obj['hash'] 68 | self.color = 'black' 69 | self.kind = 'git' 70 | self.icon = 'get?path=icons/git-icon.png' 71 | self.icon_size = 24 72 | self.font_size = 10 73 | self.zoomed_icon_size = 24 74 | self.hash = obj["hash"] 75 | self.url = obj["url"] 76 | self.message = obj["message"] 77 | self.label = obj["message"] 78 | self.author = obj["author"] 79 | self.committer = obj["committer"] 80 | self.timestamp = obj["timestamp"] 81 | self.project = obj["project"] 82 | self.changes = obj["changes"] 83 | words = self.message.split(' ') 84 | self.words = list(set(word.lower() for word in words)) 85 | dict.update(self, vars(self)) 86 | 87 | @classmethod 88 | def deserialize(cls, obj): 89 | return GitCommit(obj) 90 | 91 | def get_related_items(self): 92 | return super().get_related_items() + [ Project(self.project[0]) ] 93 | 94 | def render(self, query): 95 | return '' % self.label 96 | 97 | 98 | def cleanup(): 99 | pass 100 | 101 | 102 | def render(args): 103 | logger.info("render %s" % json.dumps(args, indent=4)) 104 | return '' % (args["url"].replace(".git", ""), args["hash"]) 105 | 106 | 107 | settings['git/can_load_more'] = True 108 | settings['git/can_delete'] = True 109 | settings['git/paths'] = [ 110 | "/Users/laffra/dev/C4E", 111 | "/Users/laffra/dev/Ikke", 112 | "/Users/laffra/dev/happymeet", 113 | "/Users/laffra/dev/wonder", 114 | ] 115 | 116 | deserialize = GitCommit.deserialize 117 | 118 | def load_repo(path): 119 | logger.debug("#"*80) 120 | logger.debug(path) 121 | repository = pydriller.Git(path) 122 | url = repository.repo.remotes[0].config_reader.get("url") 123 | for commit in repository.get_list_commits(): 124 | if not settings["git/loading"]: 125 | break 126 | obj = { 127 | "kind": "git", 128 | "url": url, 129 | "uid": "%s - %s" % (url, commit.hash), 130 | "hash": commit.hash, 131 | "label": "%s - %s - %s - %s - %s" % (url, commit.hash, commit.author.name, commit.author.email, commit.msg), 132 | "author": [ commit.author.name, commit.author.email], 133 | "committer": [ commit.committer.name, commit.committer.email], 134 | "timestamp": commit.committer_date.timestamp(), 135 | "project": [commit.project_name, commit.project_path], 136 | "message": "%s - %s" % (commit.project_name, commit.msg), 137 | "changes": [ 138 | [ 139 | modification.filename, 140 | modification.change_type.name, 141 | str(modification.complexity), 142 | str(modification.added_lines), 143 | str(modification.deleted_lines), 144 | ] 145 | for modification in commit.modified_files 146 | ], 147 | } 148 | logger.debug("Add %s" % json.dumps(obj)) 149 | storage.Storage.add_data(obj) 150 | 151 | -------------------------------------------------------------------------------- /importers/gmail.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from collections import Counter 3 | from importers import contact 4 | from importers import file 5 | from importers import Importer 6 | from importers import google_apis 7 | import datetime 8 | import email 9 | import email.header 10 | import email.utils 11 | import htmlparser 12 | import json 13 | import logging 14 | from preferences import ChromePreferences 15 | from settings import settings 16 | import os 17 | import re 18 | import stopwords 19 | import storage 20 | import time 21 | import urllib 22 | import utils 23 | from urllib.parse import urlparse 24 | 25 | 26 | MAXIMUM_THREAD_COUNT = 15 27 | URL_MATCH_RE = re.compile(r'https?://[\w\d:#@%/;$()~_?\+-=\.&]*') 28 | DATESTRING_RE = re.compile(' [-+].*') 29 | CLEANUP_FILENAME_RE = re.compile('[<>@]') 30 | MY_EMAIL_ADDRESS = ChromePreferences().get_email() 31 | GMAIL_RETRY_DELAY = 30 32 | MAX_RELATED_PERSON_COUNT = 11 33 | 34 | keys = ( 35 | 'uid', 'message_id', 'senders', 'ccs', 'receivers', 'thread', 36 | 'subject', 'label', 'content_type', 'timestamp', 37 | 'emails', 'url_domains', 'files', 38 | ) 39 | path_keys = ( 40 | 'label', 'uid', 'emails', 'files', 'url_domains', 'words' 41 | ) 42 | logging.basicConfig(level=logging.INFO) 43 | logger = logging.getLogger(__name__) 44 | gmail_service = google_apis.get_google_service("gmail", "v1") 45 | 46 | class GMail(Importer): 47 | singleton = None 48 | 49 | def __init__(self): 50 | super().__init__() 51 | self.error = None 52 | self.inbox = None 53 | self.sent = None 54 | self.kind = "gmail" 55 | GMail.singleton = self 56 | 57 | def __enter__(self): 58 | return self 59 | 60 | def __exit__(self, exc_type, exc_val, exc_tb): 61 | pass 62 | 63 | def get_attachment_path(self, uid, filename): 64 | return os.path.join(utils.cleanup_filename(uid), utils.cleanup_filename(filename)) 65 | 66 | def save_attachments(self, msg, timestamp): 67 | files = [] 68 | if not 'parts' in msg['payload']: 69 | return [] 70 | for part in msg["payload"]["parts"]: 71 | if part["mimeType"] == 'multipart': 72 | continue 73 | headers = self.parse_headers(part) 74 | disposition = headers.get('Content-Disposition') 75 | if not disposition or not disposition.startswith('attachment;'): 76 | continue 77 | filename = part["filename"] 78 | if not filename: 79 | continue 80 | attachmentId = part["body"]["attachmentId"] 81 | attachment = gmail_service.users().messages().attachments().get( 82 | userId = "me", 83 | id = attachmentId, 84 | messageId = msg["id"] 85 | ).execute() 86 | 87 | data = base64.urlsafe_b64decode(attachment['data'].encode('UTF-8')) 88 | if not data: 89 | continue 90 | file.save_file(msg['uid'], filename, timestamp, data) 91 | files.append(filename) 92 | return files 93 | 94 | @classmethod 95 | def get_body(cls, msg): 96 | payload = msg["payload"] 97 | if "parts" in payload: 98 | for part in msg["payload"]["parts"]: 99 | mime_type = part["mimeType"] 100 | if mime_type in ['text/plain', 'text/html']: 101 | if "data" in part["body"]: 102 | data = part['body']['data'].encode('UTF8') 103 | return cls.parse_body(mime_type, data) 104 | body = payload['body'] 105 | if body['size'] == 0: 106 | return '', [], '' 107 | mime_type = payload["mimeType"] 108 | data = body['data'].encode('UTF8') if 'data' in body else '' 109 | return cls.parse_body(mime_type, data) 110 | 111 | @classmethod 112 | def dump(cls, obj): 113 | with open("t.txt", "w") as fout: 114 | fout.write(json.dumps(obj, indent=4)) 115 | 116 | @classmethod 117 | def parse_body(cls, mime_type, data): 118 | body = str(base64.urlsafe_b64decode(data), "UTF8") 119 | url_domains = cls.get_domain_urls(body) 120 | if mime_type == 'text/html': 121 | body = htmlparser.get_text(body) 122 | return mime_type, url_domains, body 123 | 124 | @classmethod 125 | def get_domain_urls(cls, body): 126 | return list(set(map(cls.get_domain, re.findall(URL_MATCH_RE, body)))) 127 | 128 | @classmethod 129 | def get_domain(cls, url): 130 | return urlparse(url).netloc 131 | 132 | def load_items(self, days_after, days_before): 133 | day = days_before 134 | while settings['gmail/loading'] and day < days_after: 135 | try: 136 | self.update_days(day) 137 | try: 138 | query = "newer_than:{0}d older_than:{1}d".format(day + 1, day) 139 | result = gmail_service.users().messages().list(userId="me", maxResults=1000, q=query).execute() 140 | if "messages" in result: 141 | logger.info("Load {0} => {1} messages".format(query, len(result["messages"]))) 142 | requests = [ 143 | gmail_service.users().messages().get(userId = 'me', id = msg_id['id']) 144 | for msg_id in result["messages"] 145 | ] 146 | google_apis.batch(gmail_service, requests, self.parse_message) 147 | except urllib.error.URLError: 148 | logger.info("Gmail service timed out. Trying again in %s seconds" % GMAIL_RETRY_DELAY) 149 | time.sleep(GMAIL_RETRY_DELAY) 150 | continue 151 | contact.cleanup() 152 | logger.debug('Processed %d inbox and %d sent messages for day %d' % (0, 0, day)) 153 | except Exception as e: 154 | logger.error('Cannot load message for day %d: %s' % (day, e)) 155 | import traceback 156 | traceback.print_exc() 157 | return 158 | else: 159 | day += 1 160 | 161 | def parse_message(self, request_id, msg, exception): 162 | if not settings["gmail/loading"]: 163 | return 164 | msg["payload"]["headers"] = self.parse_headers(msg["payload"]) 165 | msg["uid"] = msg["id"] 166 | payload = msg['payload'] 167 | headers = payload['headers'] 168 | subject = self.decode_header(headers.get('Subject', msg.get("snippet", ""))) 169 | timestamp = (headers['Date'] or int(msg.get("internalDate", "0"))) / 1000 170 | content_type, url_domains, body = self.get_body(msg) 171 | label, words, rest = self.parse_email_text(subject, body) 172 | who = headers.get('To') or headers.get('From') 173 | persons = self.get_persons(timestamp, who) 174 | emails = [person.email for person in persons] 175 | names = [person.name for person in persons] 176 | thread = '%s - %s' % (label, emails) 177 | files = self.save_attachments(msg, timestamp) 178 | kind = 'gmail' 179 | if "CHAT" in msg.get("labelIds", []): 180 | kind = 'hangouts' 181 | settings.increment('%s/count' % kind) 182 | storage.Storage.add_data({ 183 | 'uid': msg['uid'] or msg['Message-ID'], 184 | 'message_id': headers.get('Message-ID', msg['uid']), 185 | 'names': names, 186 | 'emails': emails, 187 | 'thread': thread, 188 | 'subject': subject, 189 | 'label': label, 190 | 'words': words, 191 | 'rest': rest, 192 | 'content_type': content_type, 193 | 'kind': kind, 194 | 'timestamp': timestamp, 195 | 'url_domains': url_domains, 196 | 'files': files, 197 | }) 198 | 199 | def parse_headers(self, part): 200 | headers = dict([ 201 | (header["name"], header["value"]) 202 | for header in part.get("headers", []) 203 | ]) 204 | headers["Date"] = part.get("internalDate", 0) 205 | return headers 206 | 207 | def decode_header(self, s): 208 | try: 209 | text, encoding = email.header.decode_header(s)[0] 210 | return text.decode('utf8', errors='ignore') 211 | except: 212 | return s 213 | 214 | def get_persons(self, timestamp, *person_strings): 215 | return [ 216 | contact.find_contact(email_address.lower(), self.decode_header(name), timestamp=timestamp) 217 | for name, email_address in email.utils.getaddresses(filter(None, person_strings)) 218 | if email_address 219 | ] 220 | 221 | @classmethod 222 | def parse_email_text(cls, subject, body): 223 | subject_words = stopwords.remove_stopwords(subject) 224 | label = ' '.join(subject_words) 225 | body_words = [word.lower() for word in stopwords.remove_stopwords(body)] 226 | top_body_words = [word for word,count in Counter(body_words).most_common(10)] 227 | words = list(set(subject_words + top_body_words)) 228 | rest = ' '.join(set(body_words) - set(words)) 229 | logger.debug('subj: "%s"' % subject_words) 230 | logger.debug('body: "%s"' % body_words) 231 | logger.debug('all: "%s"' % words) 232 | logger.debug('rest: "%s"' % rest) 233 | return label, words, rest 234 | 235 | @classmethod 236 | def get_status(cls): 237 | return Importer.get_status("gmail", "email") 238 | 239 | 240 | class GMailNode(storage.Data): 241 | def __init__(self, obj): 242 | super(GMailNode, self).__init__(obj.get('label', '')) 243 | self.uid = obj['uid'] 244 | self.message_id = obj.get('message_id', '') 245 | self.color = 'darkred' 246 | self.names = obj.get('names', []) 247 | self.emails = obj.get('emails', []) 248 | self.timestamp = obj.get('timestamp', 0) 249 | self.persons = list(filter(None, [contact.find_contact(email, timestamp=self.timestamp) for email in self.emails])) 250 | self.in_reply_to = obj.get('in_reply_to', '') 251 | self.subject = obj.get('subject', '') 252 | self.rest = obj.get('rest', '') 253 | self.kind = obj.get('kind') 254 | self.icon = 'get?path=icons/gmail-icon.png' 255 | self.icon_size = 24 256 | self.font_size = 10 257 | self.zoomed_icon_size = 24 258 | self.thread = obj.get('thread') 259 | self.node_size = 1 260 | self.url_domains = obj.get('url_domains', []) 261 | self.files = list(filter(lambda file: not file.endswith(".ics"), obj.get('files', []))) 262 | self.label = self.label or ' '.join(self.words + [str(len(self.files))]) 263 | self.words = self.label.split() or list(sorted(obj.get('words', []))) or self.subject.split() 264 | self.connected = False 265 | dict.update(self, vars(self)) 266 | 267 | def __hash__(self): 268 | return hash(self.uid) 269 | 270 | def is_related_item(self, other): 271 | if False and self.url_domains and other.kind == 'browser': 272 | related = other.domain == self.url_domains[0] 273 | elif other.kind == 'gmail': 274 | related = not self.connected and self.label == other.label 275 | self.connected = other.connected = True 276 | elif self.files and other.kind == 'file': 277 | related = other.filename in self.files 278 | elif self.persons and other.kind == 'contact': 279 | related = other in self.persons 280 | else: 281 | related = False 282 | logger.debug("related? %s %s %s %s" % (repr(self.label), self.files, repr(other.label), related)) 283 | return related 284 | 285 | def get_related_items(self): 286 | files = [file.load_file(self.uid, filename) for filename in self.files] 287 | return super().get_related_items() + files + self.persons[:MAX_RELATED_PERSON_COUNT] 288 | 289 | def is_duplicate(self, duplicates): 290 | key = "gmail - %s" % ' '.join(sorted(word for word in self.words if not stopwords.is_stopword(word))) 291 | if key in duplicates: 292 | self.mark_duplicate() 293 | return True 294 | duplicates.add(key) 295 | return False 296 | 297 | 298 | def _render(args): 299 | url = 'https://mail.google.com/mail/u/0/#search/rfc822msgid:%s' % args["message_id"] 300 | return '' % url 301 | 302 | 303 | def render(args): 304 | import re 305 | words = list(filter(lambda word: re.match("^[a-zA-Z]*$", word), args["subject"].split()))[:10] 306 | logger.info(args["subject"]) 307 | logger.info(words) 308 | url = 'https://mail.google.com/mail/u/0/#search/%s' % urllib.parse.quote(' '.join(words)) 309 | return '' % url 310 | 311 | def load(): 312 | GMail.singleton.load() 313 | 314 | def poll(): 315 | load() 316 | 317 | def delete_all(): 318 | pass 319 | 320 | 321 | settings['gmail/can_load_more'] = True 322 | settings['gmail/pending'] = 'gmail/username' not in settings 323 | 324 | GMail.singleton = GMail() 325 | get_status = GMail.get_status 326 | 327 | 328 | def deserialize(obj): 329 | return GMailNode(obj) 330 | 331 | 332 | def cleanup(): 333 | pass 334 | 335 | def test(): 336 | global DAYS_LOAD 337 | global MAXIMUM_DAYS_LOAD 338 | logger.info("############### start") 339 | settings.clear() 340 | MAXIMUM_DAYS_LOAD = DAYS_LOAD = 1 341 | logger.info("############### load") 342 | load() 343 | logger.info("############### poll 1") 344 | poll() 345 | logger.info("############### poll 2") 346 | poll() 347 | logger.info("############### poll 3") 348 | poll() 349 | logger.info("############### done") 350 | 351 | -------------------------------------------------------------------------------- /importers/gmail_credentials.json: -------------------------------------------------------------------------------- 1 | {"installed":{"client_id":"698334219404-89ti8n0k679r8jehvdto1t0n4jp5jn28.apps.googleusercontent.com","project_id":"ikke-1538916647661","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"iVFEkUofJPudltKen80nnoTQ","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}} -------------------------------------------------------------------------------- /importers/google_apis.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import utils 4 | import preferences 5 | 6 | from googleapiclient.discovery import build 7 | from google_auth_oauthlib.flow import InstalledAppFlow 8 | from google.auth.transport.requests import Request 9 | from googleapiclient.http import BatchHttpRequest 10 | 11 | SCOPES = [ 12 | 'https://www.googleapis.com/auth/gmail.readonly', 13 | 'https://www.googleapis.com/auth/calendar.readonly', 14 | 'https://www.googleapis.com/auth/contacts.readonly', 15 | 'https://www.googleapis.com/auth/photoslibrary.readonly', 16 | 'https://www.googleapis.com/auth/drive.readonly', 17 | ] 18 | 19 | def get_google_service(api, version): 20 | creds = None 21 | email = preferences.ChromePreferences().get_email() 22 | token_path = os.path.join(utils.HOME_DIR, 'google_token_%s.pickle' % email) 23 | if os.path.exists(token_path): 24 | with open(token_path, 'rb') as token: 25 | creds = pickle.load(token) 26 | if not creds or not creds.valid: 27 | if creds and creds.expired and creds.refresh_token: 28 | creds.refresh(Request()) 29 | else: 30 | flow = InstalledAppFlow.from_client_secrets_file('importers/gmail_credentials.json', SCOPES) 31 | creds = flow.run_local_server(port=0) 32 | with open(token_path, 'wb') as token: 33 | pickle.dump(creds, token) 34 | return build(api, version, credentials=creds) 35 | 36 | 37 | def batch(service, requests, callback): 38 | for chunk in chunks(requests, 100): 39 | batch = service.new_batch_http_request() 40 | for request in chunk: 41 | batch.add(request, callback=callback) 42 | batch.execute() 43 | 44 | 45 | def chunks(elements, chunkSize): 46 | for n in range(0, len(elements), chunkSize): 47 | yield elements[n:n + chunkSize] -------------------------------------------------------------------------------- /importers/hangouts.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from settings import settings 3 | import storage 4 | from importers import Importer, gmail 5 | import urllib 6 | 7 | keys = ('uid', 'timestamp') 8 | logger = logging.getLogger(__name__) 9 | 10 | def deserialize(obj): 11 | return HangoutNode(obj) 12 | 13 | def get_status(): 14 | return Importer.get_status("hangouts", "message") 15 | 16 | 17 | def delete_all(): 18 | pass 19 | 20 | 21 | def poll(): 22 | pass 23 | 24 | 25 | def can_load_more(): 26 | return False 27 | 28 | 29 | def load(): 30 | pass 31 | 32 | 33 | class HangoutNode(gmail.GMailNode): 34 | def __init__(self, obj): 35 | super(HangoutNode, self).__init__(obj) 36 | self.kind = 'hangouts' 37 | self.color = 'green' 38 | self.icon = 'get?path=icons/hangouts-icon.png' 39 | dict.update(self, vars(self)) 40 | 41 | @classmethod 42 | def deserialize(cls, obj): 43 | return HangoutNode(obj) 44 | 45 | def is_related_item(self, other): 46 | return False 47 | 48 | def render(self, query): 49 | return '' % self.label 50 | 51 | 52 | def cleanup(): 53 | pass 54 | 55 | 56 | def render(args): 57 | import re 58 | words = list(filter(lambda word: re.match("^[a-zA-Z]*$", word), args["subject"].split()))[:10] 59 | url = 'https://mail.google.com/mail/u/0/#search/in:chats %s' % urllib.parse.quote(' '.join(words)) 60 | # url = 'https://mail.google.com/chat/u/0/#search/%s' % urllib.parse.quote(' '.join(words)) 61 | return '' % url 62 | 63 | 64 | settings['hangouts/can_load_more'] = False 65 | settings['hangouts/can_delete'] = True 66 | 67 | deserialize = HangoutNode.deserialize 68 | -------------------------------------------------------------------------------- /importers/quickstart.py: -------------------------------------------------------------------------------- 1 | 2 | def main(): 3 | service = get_gmail_service() 4 | 5 | # Call the Gmail API 6 | 7 | if __name__ == '__main__': 8 | main() -------------------------------------------------------------------------------- /installation/ikke.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | Ikke 7 | 8 | Program 9 | /Applications/Ikke.app 10 | 11 | ProcessType 12 | Interactive 13 | 14 | RunAtLoad 15 | 16 | 17 | KeepAlive 18 | 19 | 20 | -------------------------------------------------------------------------------- /installer.py: -------------------------------------------------------------------------------- 1 | import os 2 | from settings import settings 3 | import time 4 | import utils 5 | 6 | 7 | def install(): 8 | if os.name == 'posix': 9 | plist_path = os.path.join(utils.INSTALL_FOLDER, 'installation', 'ikke.plist') 10 | library_path = os.path.expanduser('~/Library/LaunchAgents/ikke.plist') 11 | with open(library_path, 'w') as fout: 12 | fout.write(open(plist_path).read()) 13 | os.system('launchctl load -w %s' % library_path) 14 | settings['installed'] = time.time() 15 | 16 | 17 | if __name__ == '__main__': 18 | install() -------------------------------------------------------------------------------- /localsearch/search.vbs: -------------------------------------------------------------------------------- 1 | On Error Resume Next 2 | 3 | if WScript.Arguments.Count = 0 Then 4 | WScript.Echo "Usage: cscript search.vbs scope query" 5 | WScript.Quit 6 | end if 7 | 8 | SqlQuery = "SELECT Top 100000 System.ItemPathDisplay " & _ 9 | "FROM SYSTEMINDEX WHERE FREETEXT('" & WScript.Arguments.Item(1) & "') AND " & _ 10 | "SCOPE = '" & WScript.Arguments.Item(0) & "' AND " & _ 11 | "System.DateModified >= '" & Wscript.Arguments.Item(2) & "'" 12 | 13 | Set objConnection = CreateObject("ADODB.Connection") 14 | Set objRecordSet = CreateObject("ADODB.Recordset") 15 | 16 | objConnection.Open "Provider=Search.CollatorDSO;Extended Properties='Application=Windows';" 17 | 18 | objRecordSet.Open SqlQuery, objConnection 19 | 20 | objRecordSet.MoveFirst 21 | Do Until objRecordset.EOF 22 | Wscript.Echo objRecordset.Fields.Item("System.ItemPathDisplay") 23 | objRecordset.MoveNext 24 | Loop -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import rumps 2 | import faulthandler 3 | import logging 4 | import os 5 | import threading 6 | import webbrowser 7 | import pubsub 8 | import pynsights 9 | from server import startServer 10 | 11 | faulthandler.enable() 12 | 13 | logger = logging.getLogger('main') 14 | lock = threading.Lock() 15 | 16 | class App(rumps.App): 17 | ''' 18 | The main UI for Ikke. Places a menu item in the MacOS statusbar. 19 | ''' 20 | def __init__(self): 21 | super(App, self).__init__("ⓘ") 22 | self.menu = [ ] 23 | self.menu._menu.setDelegate_(self) 24 | self.related_items = [] 25 | self.query = "" 26 | startServer(self.show_related) 27 | pubsub.register("related", self.show_related) 28 | pubsub.register("search", self.search_started) 29 | threading.Timer(1, self.create_menu).start() 30 | rumps.notification("Ikke", "Welcome", "See the number in the statusbar for related items") 31 | 32 | def show_related(self, query, related_items): 33 | ''' 34 | Show related items to a given query in the MacOS statusbar 35 | ''' 36 | with lock: 37 | logger.info("Show related %d items for '%s'", len(related_items), query) 38 | if not query: 39 | return 40 | self.query = query.replace("Chris Laffra", "") 41 | self.related_items = related_items 42 | self.create_menu() 43 | 44 | def search_started(self, query, _): 45 | ''' 46 | Handle a new search 47 | ''' 48 | if not query: 49 | return 50 | self.title = "ⓘ" 51 | logger.info("Search started for '%s'", query) 52 | 53 | @classmethod 54 | def open_url(cls, url): 55 | ''' 56 | Open a given URL in the default web browser 57 | ''' 58 | logger.info("Open URL: %s", url) 59 | webbrowser.open(url) 60 | 61 | def search_ikke(self): 62 | ''' 63 | Run the current search query in the Ikke graph UI. 64 | This will open a new browser page. 65 | ''' 66 | App.open_url("http://localhost:1964/?q=" + self.query) 67 | 68 | def settings(self): 69 | ''' 70 | Open the settings UI for Ikke. 71 | ''' 72 | App.open_url("http://localhost:1964/settings") 73 | 74 | def help(self): 75 | ''' 76 | Open the documentation for Ikke. 77 | ''' 78 | App.open_url("https://github.com/laffra/Ikke") 79 | 80 | def problem(self): 81 | ''' 82 | Open the issues page for Ikke. 83 | ''' 84 | App.open_url("https://github.com/laffra/Ikke/issues") 85 | 86 | def search_google(self): 87 | ''' 88 | Open a browser window to Google to search for the current query. 89 | ''' 90 | App.open_url("https://google.com/search?q=" + self.query) 91 | 92 | def get_icon(self, item): 93 | ''' 94 | Return the relative path for the icon in the given menu item. 95 | ''' 96 | file = "" 97 | if item.icon.startswith("get"): 98 | file = "html/" + item.icon.replace("get?path=", "") 99 | if item.icon == "undefined": 100 | file = "html/icons/blue-circle.png" 101 | return file if os.path.exists(file) else "html/icons/browser-web-icon.png" 102 | 103 | def create_menu_item(self, item): 104 | ''' 105 | Create a new menu item to be shown in the MacOS statusbar. 106 | ''' 107 | return rumps.MenuItem( 108 | item.label or item.title, 109 | icon = self.get_icon(item), 110 | callback = lambda menuItem: self.open_related(item), 111 | ) 112 | 113 | def create_menu(self): 114 | ''' 115 | Create the menu to be shown in the MacOS statusbar. 116 | ''' 117 | self.menu.clear() 118 | self.title = "%d" % (len(self.related_items)) 119 | short_query = " ".join(self.query.split()[:5]) 120 | menu_items = [ 121 | rumps.MenuItem('Search Ikke for "%s"' % short_query, lambda _: self.search_ikke()), 122 | rumps.MenuItem('Search Google for "%s"' % short_query, lambda _: self.search_google()), 123 | None, 124 | ] if short_query else [] 125 | menu_items += [ 126 | self.create_menu_item(item) 127 | for item in self.related_items 128 | ] + [ 129 | None, 130 | rumps.MenuItem("Ikke Settings", lambda _: self.settings()), 131 | rumps.MenuItem("Help", lambda _: self.help()), 132 | rumps.MenuItem("Report a Problem", lambda _: self.problem()), 133 | rumps.MenuItem("Quit", self.quit), 134 | ] 135 | self.menu = menu_items 136 | 137 | def quit(self, sender=None): 138 | pynsights.stop_tracing() 139 | rumps.quit_application() 140 | 141 | def open_related(self, item): 142 | ''' 143 | The user clicked on a related item. Open it now. 144 | ''' 145 | self.open_url("http://localhost:1964/render?query=%s&%s" % ( 146 | self.query, 147 | "&".join("%s=%s" % (key, item) for key, item in item.items()), 148 | )) 149 | 150 | def menuWillOpen_(self): 151 | self.create_menu() 152 | 153 | def handleRelated(self): 154 | rumps.notification("Ikke", "Related Items", "Based on your history, this item is related") 155 | 156 | 157 | if __name__ == "__main__": 158 | App().run() 159 | -------------------------------------------------------------------------------- /memory.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import psutil 4 | import sys 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | KB = 1024 9 | MB = KB * KB 10 | GB = 1024 * MB 11 | TB = 1024 * GB 12 | 13 | MEMORY_SIZES = [ "TB", "GB", "MB", "KB" ] 14 | 15 | 16 | def toHuman(bytes): 17 | for size in MEMORY_SIZES: 18 | value = globals()[size] 19 | if bytes >= value: 20 | return f"{bytes / value:.1f}{size}" 21 | return f"{bytes} bytes" 22 | 23 | 24 | def usage(human=False): 25 | process = psutil.Process(os.getpid()) 26 | memory = process.memory_info().rss 27 | return toHuman(memory) if human else memory 28 | 29 | 30 | def check(max, restart=True): 31 | memory = usage() 32 | # handle unfixable memory leak caused by rumps 33 | if memory > max: 34 | logger.info(f"Current memory usage is {toHuman(memory)}, which is larger than {toHuman(max)}.") 35 | if restart: 36 | os.execl(sys.executable, os.path.abspath(__file__), *sys.argv) 37 | else: 38 | logger.info(f"Current memory usage: {toHuman(memory)}, which is less than {toHuman(max)}.") 39 | 40 | 41 | if __name__ == "__main__": 42 | logger.info(toHuman(340)) 43 | logger.info(toHuman(2.5*KB)) 44 | logger.info(toHuman(2.5*MB)) 45 | logger.info(toHuman(2.5*GB + 2.5*MB)) 46 | logger.info(toHuman(2.5*TB)) 47 | check(GB, restart=False) 48 | -------------------------------------------------------------------------------- /poller.py: -------------------------------------------------------------------------------- 1 | import importers 2 | import logging 3 | import pkgutil 4 | import storage 5 | import time 6 | import threading 7 | import memory 8 | 9 | from importers import download 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | POLL_SLEEP_INTERVAL_SECONDS = 600 14 | POLL_SLEEP_INCREMENT_SECONDS = 60 15 | 16 | workers = [] 17 | 18 | class Worker(threading.Thread): 19 | 20 | def __init__(self, importer): 21 | super(Worker, self).__init__() 22 | self.importer = importer 23 | self.running = True 24 | self.index = len(workers) 25 | workers.append(self) 26 | 27 | def run(self): 28 | delay = POLL_SLEEP_INCREMENT_SECONDS * self.index 29 | logger.info("Staggered start %s for %d seconds" % (self.importer.__name__, delay)) 30 | time.sleep(delay) 31 | while self.running: 32 | self.sleep() 33 | self.poll() 34 | memory.check(memory.GB/2) 35 | 36 | def sleep(self): 37 | delay = POLL_SLEEP_INTERVAL_SECONDS 38 | logger.info("Sleeping %s for %d seconds" % (self.importer.__name__, delay)) 39 | for n in range(delay): 40 | time.sleep(1) 41 | if not self.running: 42 | break 43 | 44 | def poll(self): 45 | logging.info('Poll running=%s' % self.running) 46 | if self.running: 47 | logging.info('Polling importers') 48 | try: 49 | self.importer.poll() 50 | logging.debug('Polling %s' % self.importer.__name__) 51 | except Exception as e: 52 | logging.error('POLLER: Error polling %s: %s' % (self.importer.__name__, e)) 53 | logging.info('Polling Storage') 54 | storage.Storage.poll() 55 | 56 | def stop(self): 57 | self.running = False 58 | 59 | workers = [ 60 | Worker(getattr(importers, name)) 61 | for _, name, _ in pkgutil.iter_modules(['importers']) 62 | if hasattr(importers, name) 63 | ] 64 | 65 | 66 | 67 | 68 | def poll(): 69 | for worker in workers: 70 | worker.poll() 71 | 72 | 73 | def start(): 74 | for worker in workers: 75 | worker.start() 76 | 77 | 78 | def stop(): 79 | for worker in workers: 80 | worker.stop() 81 | -------------------------------------------------------------------------------- /preferences.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | 5 | def get_preferences_path(): 6 | if os.name == 'nt': 7 | return ['AppData', 'Local', 'Google', 'Chrome', 'User Data', 'Default', 'preferences'] 8 | else: 9 | return ['Library', 'Application Support', 'Google', 'Chrome', 'Default', 'Preferences'] 10 | 11 | 12 | class ChromePreferences: 13 | def __init__(self): 14 | home = os.path.expanduser("~") 15 | preferences_path = os.path.join(home, *get_preferences_path()) 16 | with open(preferences_path) as fin: 17 | self.preferences = json.load(fin) 18 | print("Loaded Chrome preferences from", preferences_path) 19 | 20 | def get_language_profile(self): 21 | return [(l['language'], l['probability']) for l in self.get('language_profile').get('reading').get('preference')] 22 | 23 | def get_google_url(self): 24 | return self.get('browser').get('last_known_google_url') 25 | 26 | def get_top_sites(self): 27 | sites = self.get('profile').get('content_settings').get('exceptions').get('site_engagement') 28 | return sorted([ 29 | (url.split(',')[0], data['setting']['rawScore']) 30 | for url, data in sites.items() 31 | ], key=lambda pair: -pair[1]) 32 | 33 | def get_account_info(self): 34 | return self.get('account_info')[-1] 35 | 36 | def get_email(self): 37 | return self.get_account_info()['email'] 38 | 39 | def get_custom_handlers(self): 40 | return { 41 | handler['protocol']: handler['url'] 42 | for handler in self.get('custom_handlers').get('registered_protocol_handlers') 43 | } 44 | 45 | def get_account_infos(self): 46 | return self.get('account_info') 47 | 48 | def get_chrome_version(self): 49 | return self.get('extensions').get('last_chrome_version') 50 | 51 | def get_download_folder(self): 52 | return self.get('savefile').get('default_directory') 53 | 54 | def get(self, key): 55 | return self.preferences[key] 56 | 57 | def __str__(self): 58 | return json.dumps(self.preferences, indent=4) 59 | 60 | 61 | if __name__ == '__main__': 62 | p = ChromePreferences() 63 | print('language_profile:', p.get_language_profile()) 64 | print('custom_handlers:', p.get_custom_handlers()) 65 | print('top_sites[:10]:') 66 | for n, (url, score) in enumerate(p.get_top_sites()[:10]): 67 | print(' ', n+1, url, score) 68 | print('full_name:', p.get_account_info().get('full_name')) 69 | print('account_info:',json.dumps(p.get_account_info(), indent=4)) 70 | 71 | import hosts 72 | hosts.setup_as_administrator() 73 | 74 | -------------------------------------------------------------------------------- /pubsub.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import logging 3 | 4 | logger = logging.getLogger('pubsub') 5 | handlers = defaultdict(set) 6 | 7 | def register(event, handler): 8 | handlers[event].add(handler) 9 | 10 | def unregister(event, handler): 11 | del handlers[event] 12 | 13 | def notify(event, *arguments): 14 | logger.info("###### Notify: %s: %s" % (event, arguments)) 15 | for handler in handlers[event]: 16 | handler(*arguments) -------------------------------------------------------------------------------- /pyinstaller.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | block_cipher = None 4 | 5 | 6 | a = Analysis(['server.py'], 7 | pathex=['/Users/laffra/dev/Ikke'], 8 | binaries=[], 9 | datas=[ 10 | ('installation/*', 'installation'), 11 | ('localsearch/*', 'localsearch'), 12 | ('importers/*', 'gmail_credentials.json'), 13 | ('html/*', 'html'), 14 | ('html/icons/*', 'html/icons'), 15 | ('html/3rd/*', 'html/3rd'), 16 | ('html/3rd/jquery-ui-1.12.1/*', 'html/3rd/jquery-ui-1.12.1'), 17 | ('html/3rd/jquery-ui-1.12.1/external/*', 'html/3rd/jquery-ui-1.12.1/external'), 18 | ('html/3rd/jquery-ui-1.12.1/external/jquery/*', 'html/3rd/jquery-ui-1.12.1/external'), 19 | ('html/3rd/jquery-ui-1.12.1/images/*', 'html/3rd/jquery-ui-1.12.1/images'), 20 | ], 21 | hiddenimports=[ 22 | 'importers', 23 | 'importers.browser', 24 | 'importers.calendar', 25 | 'importers.contact', 26 | 'importers.download', 27 | 'importers.file', 28 | 'importers.git', 29 | 'importers.gmail', 30 | 'importers.google_apis', 31 | 'importers.hangouts', 32 | 'importers.quickstart', 33 | ], 34 | hookspath=[], 35 | runtime_hooks=[], 36 | excludes=[], 37 | win_no_prefer_redirects=False, 38 | win_private_assemblies=False, 39 | cipher=block_cipher) 40 | pyz = PYZ(a.pure, a.zipped_data, 41 | cipher=block_cipher) 42 | exe = EXE(pyz, 43 | a.scripts, 44 | a.binaries, 45 | a.zipfiles, 46 | a.datas, 47 | name='ikke', 48 | debug=False, 49 | strip=False, 50 | upx=True, 51 | runtime_tmpdir=None, 52 | console=False ) 53 | app = BUNDLE(exe, 54 | name='ikke.app', 55 | icon=None, 56 | bundle_identifier=None) 57 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import re 3 | 4 | from stopwords import is_stopword, remove_stopwords 5 | from storage import Storage 6 | import pubsub 7 | import memory 8 | 9 | from importers import INITIAL_DAYS_LOAD, browser 10 | from importers import contact 11 | from importers import download 12 | from importers import file 13 | from importers import gmail 14 | import installer 15 | import logging 16 | import graph 17 | import poller 18 | from preferences import ChromePreferences 19 | from settings import settings 20 | from http.server import BaseHTTPRequestHandler 21 | from http.server import HTTPServer 22 | from socketserver import ThreadingMixIn 23 | from urllib.parse import parse_qs, urlparse 24 | from urllib.request import urlopen 25 | import jinja2 26 | import json 27 | import os 28 | import subprocess 29 | import sys 30 | import traceback 31 | import threading 32 | import utils 33 | 34 | import pynsights 35 | 36 | logger = logging.getLogger('server') 37 | 38 | PORT_NUMBER = 1964 39 | SERVER_ADDRESS = '127.0.0.1' 40 | 41 | 42 | class Server(BaseHTTPRequestHandler): 43 | jinja2_env = jinja2.Environment( 44 | loader = jinja2.FileSystemLoader(os.path.join(utils.INSTALL_FOLDER, 'html')) 45 | ) 46 | graphs = {} 47 | path = '' 48 | args = {} 49 | preferences = ChromePreferences() 50 | 51 | def do_GET(self): 52 | routes = { 53 | '/': self.get_index, 54 | '/clear': self.clear, 55 | '/stopclear': self.stop_deleting, 56 | '/status': self.status, 57 | '/load': self.load, 58 | '/stopload': self.stop_loading, 59 | '/get_related_items': self.get_related_items, 60 | '/extensions': self.extensions, 61 | '/get': self.get_resource, 62 | '/get_image': self.get_image, 63 | '/render': self.render, 64 | '/open': self.open_local, 65 | '/settings': self.settings, 66 | '/settings_set': self.settings_set, 67 | '/settings_get': self.settings_get, 68 | '/projects': self.projects, 69 | '/jquery.js': self.get_jquery, 70 | '/search': self.search, 71 | '/graph': self.get_graph, 72 | '/poll': self.poll, 73 | '/favicon.ico': self.favicon, 74 | } 75 | logger.debug('GET %s' % self.path) 76 | self.parse_args() 77 | routes.get(self.path, self.get_file)() 78 | 79 | def log_message(self, format, *args): 80 | message = format % args 81 | if not 'search_poll' in message: 82 | logger.debug(message) 83 | 84 | def parse_args(self): 85 | try: 86 | index = self.path.index('?') 87 | self.args = parse_qs(self.path[index + 1:]) 88 | for k,v in self.args.items(): 89 | self.args[k] = v[0] if len(v) == 1 else v 90 | self.path = self.path[:index] 91 | except ValueError: 92 | self.args = {} 93 | 94 | def settings(self): 95 | pynsights.annotate("[settings]") 96 | html = self.jinja2_env.get_template('settings.html').render({ 97 | 'memory': memory.usage(human=True), 98 | 'location': utils.INSTALL_FOLDER, 99 | 'kinds': graph.ALL_ITEM_KINDS[1:], 100 | 'can_load_more': { kind: Storage.can_load_more(kind) for kind in graph.ALL_ITEM_KINDS[1:]}, 101 | 'can_delete': { kind: Storage.can_delete(kind) for kind in graph.ALL_ITEM_KINDS[1:]}, 102 | }) 103 | self.respond(html) 104 | memory.check(memory.GB/2) 105 | 106 | def settings_set(self): 107 | key = self.args["key"] 108 | value = self.args["value"] 109 | settings[key] = value 110 | self.respond("Ikke settings: set %s to '%s'" % (key, value)) 111 | 112 | def settings_get(self): 113 | self.respond(settings[self.args["key"]]) 114 | 115 | def projects(self): 116 | data = [ 117 | "Personal", 118 | "Work", 119 | { 120 | "id" : "history", 121 | "text" : "History", 122 | "state" : { "opened" : True }, 123 | "children" : [ 124 | { 125 | "text": self.args["q"], 126 | "state" : { "selected" : True }, 127 | }, 128 | ] 129 | } 130 | ] 131 | self.respond(json.dumps(data)) 132 | 133 | def respond(self, html, content_type=None): 134 | self.send_response(200) 135 | if content_type: 136 | self.send_header('Content-type', content_type) 137 | self.end_headers() 138 | try: 139 | message = bytes(html, 'utf8') 140 | except: 141 | message = html 142 | try: 143 | self.wfile.write(message) 144 | except TypeError: 145 | pass # ignore 146 | except Exception as e: 147 | logger.error('Client went away: %s: %s' % (type(e), e)) 148 | 149 | def get_index(self): 150 | html = self.jinja2_env.get_template('index.html').render({ 151 | 'query': self.args.get('q', ''), 152 | 'kinds': graph.ALL_ITEM_KINDS, 153 | 'kinds_string': repr(graph.ALL_ITEM_KINDS), 154 | 'account': self.preferences.get_account_info(), 155 | }) 156 | logger.info(self.preferences.get_account_info()) 157 | self.respond(html) 158 | 159 | def clear(self): 160 | kind = self.args.get('kind', '') 161 | logger.info('Clearing all history for %s' % kind) 162 | if Storage.delete_all(kind): 163 | self.respond('OK') 164 | 165 | def stop_deleting(self): 166 | Storage.stop_deleting(self.args['kind']) 167 | self.respond('OK') 168 | 169 | def status(self): 170 | self.respond(json.dumps(Storage.get_status())) 171 | 172 | def search(self): 173 | query = self.args.get('q', '') 174 | pynsights.annotate("[search]") 175 | email = self.args['email'] 176 | settings['query'] = query 177 | duration = self.args.get('duration', 'year') 178 | logger.info('search %s %s %s' % (email, duration, query)) 179 | self.graphs[query] = graph.Graph(email, query, duration) 180 | self.respond('OK') 181 | 182 | def stop_loading(self): 183 | Storage.stop_loading(self.args['kind']) 184 | self.respond('OK') 185 | 186 | def load(self): 187 | Storage.load(self.args['kind']) 188 | self.respond('OK') 189 | memory.check(memory.GB/2) 190 | 191 | def poll(self): 192 | poller.poll() 193 | 194 | def get_graph(self): 195 | query = self.args.get('q') 196 | pynsights.annotate("[graph]") 197 | keep_duplicates = self.args.get('d', '0') == '1' 198 | kind = self.args['kind'] 199 | logger.info('get graph for %s: %s', kind, query) 200 | graph = self.graphs[query].get_graph(kind, keep_duplicates) 201 | logger.debug("Graph: %s" % json.dumps(graph, indent=4)) 202 | self.respond(json.dumps(graph)) 203 | 204 | def favicon(self): 205 | self.args["path"] = 'icons/favicon.ico' 206 | return self.get_resource() 207 | 208 | def get_resource(self): 209 | path = os.path.join(utils.INSTALL_FOLDER, 'html', self.args['path']) 210 | self.respond(self.load_resource(path, 'rb')) 211 | 212 | def get_image(self): 213 | filename = self.args['path'] 214 | if not "/" in filename: 215 | filename = os.path.join(filename, self.args.get("filename", "No filename in %s" % json.dumps(self.args))) 216 | path = os.path.join(utils.FILE_DIR, filename) 217 | self.respond(self.load_resource(path, 'rb')) 218 | 219 | def render(self): 220 | pynsights.annotate("[render]") 221 | try: 222 | logger.info("render %s" % json.dumps(self.args, indent=4)) 223 | handler = Storage.get_handler(self.args["kind"]) 224 | self.respond('%s

Args:

%s
' % ( 225 | handler.render(self.args), 226 | json.dumps(self.args, indent=4), 227 | )) 228 | except: 229 | msg = 'Cannot render: %s' % traceback.format_exc() 230 | logging.error(msg) 231 | self.respond(msg) 232 | 233 | def get_jquery(self): 234 | self.respond(self.load_resource('jquery.js', 'rb')) 235 | 236 | def extensions(self): 237 | html = self.jinja2_env.get_template('extensions.html').render({ 238 | 'location': os.path.join(utils.INSTALL_FOLDER, 'extension') 239 | }) 240 | self.respond(html) 241 | 242 | def load_items(self): 243 | items = Storage.search_file('"%s"' % self.args['uid']) 244 | for n, item in enumerate(items): 245 | if 'path' in item and os.path.exists(item['path']): 246 | path = os.path.realpath(item['path']) 247 | url = 'file://%s' % path 248 | import webbrowser 249 | webbrowser.open(url) 250 | html = '' 251 | self.respond(html) 252 | 253 | def open_local(self): 254 | path = os.path.join(utils.FILE_DIR, self.args["path"]) 255 | logger.info("Open %s" % path) 256 | subprocess.call(['open', path]) 257 | 258 | def get_related_items(self): 259 | browser.save_image( 260 | self.args.get('url', ''), 261 | self.args.get('title', ''), 262 | self.args.get('image', ''), 263 | self.args.get('favicon', ''), 264 | self.args.get('selection', ''), 265 | self.args.get('essence', ''), 266 | self.args.get('keywords', ''), 267 | float(self.args.get('timestamp', datetime.datetime.utcnow().timestamp())) 268 | ) 269 | self.notify_related() 270 | memory.check(memory.GB/2) 271 | 272 | def notify_related(self): 273 | query = re.sub("[^A-Za-z_0-9]", " ", self.args.get('essence', self.args.get('title', ''))) 274 | words = [word for word in query.split() if not is_stopword(word)] 275 | query = " ".join(words) 276 | days_to_search = 365 277 | results = Storage.search(query, days_to_search) 278 | if not results: 279 | for word in words: 280 | results += Storage.search(word, days_to_search) 281 | results = filter(lambda result: result.kind != "time", results) 282 | results = sorted(results, key=lambda result: -result.timestamp) 283 | logger.info("Found %d related results" % len(results)) 284 | from classify import remove_duplicates 285 | results = remove_duplicates(results, False) 286 | results = results[:30] 287 | results = sorted(results, key=lambda result: result.kind, reverse=True) 288 | pubsub.notify("related", query, results) 289 | self.respond(json.dumps({ 290 | "query": query, 291 | "items": results, 292 | })) 293 | 294 | def get_file(self): 295 | try: 296 | payload = self.load_resource(self.path[1:]) 297 | try: 298 | payload = bytes(payload, 'utf8') 299 | except: 300 | pass 301 | self.respond(payload) 302 | except Exception as e: 303 | logger.debug('Fail on %s: %s' % (self.path, e)) 304 | self.send_response(404) 305 | 306 | def load_resource(self, filename, format='r'): 307 | dirname = os.path.dirname(filename).replace(' ', '+') 308 | basename = os.path.basename(filename) 309 | filename = os.path.join(dirname, basename) 310 | path = os.path.join(utils.INSTALL_FOLDER, os.path.join('html', filename)) 311 | with open(path, format) as fp: 312 | return fp.read() 313 | 314 | 315 | class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): 316 | """Handle requests in a separate thread.""" 317 | 318 | 319 | def runServerInBackground(server): 320 | poller.start() 321 | server.serve_forever() 322 | poller.stop() 323 | for kind in graph.ALL_ITEM_KINDS[1:]: 324 | Storage.stop_loading(kind) 325 | 326 | def startServer(handleRelated): 327 | logging.basicConfig(level=logging.INFO) 328 | installer.install() 329 | port = settings.get('port', PORT_NUMBER) 330 | threaded_server = ThreadedHTTPServer(('localhost', port), Server) 331 | threaded_server.handleRelated = handleRelated 332 | 333 | settings['port'] = port 334 | if settings['browser/count'] < 100: 335 | threading.Thread(target=lambda: Storage.load('browser')).start() 336 | threading.Thread(target=lambda: runServerInBackground(threaded_server)).start() 337 | 338 | if __name__ == '__main__': 339 | if "clear" in sys.argv: 340 | print("Clearing...") 341 | settings.clear() 342 | else: 343 | def handleRelated(query, relatedItems): 344 | print("Related:", query, len(relatedItems)) 345 | startServer(handleRelated) 346 | -------------------------------------------------------------------------------- /server.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | block_cipher = None 5 | 6 | 7 | a = Analysis(['server.py'], 8 | pathex=['/Users/laffra/dev/Ikke'], 9 | binaries=[], 10 | datas=[], 11 | hiddenimports=[], 12 | hookspath=[], 13 | runtime_hooks=[], 14 | excludes=[], 15 | win_no_prefer_redirects=False, 16 | win_private_assemblies=False, 17 | cipher=block_cipher, 18 | noarchive=False) 19 | pyz = PYZ(a.pure, a.zipped_data, 20 | cipher=block_cipher) 21 | exe = EXE(pyz, 22 | a.scripts, 23 | a.binaries, 24 | a.zipfiles, 25 | a.datas, 26 | [], 27 | name='server', 28 | debug=False, 29 | bootloader_ignore_signals=False, 30 | strip=False, 31 | upx=True, 32 | upx_exclude=[], 33 | runtime_tmpdir=None, 34 | console=True ) 35 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | import utils 5 | import threading 6 | 7 | 8 | SETTINGS_PATH = os.path.join(utils.HOME_DIR, 'settings.json') 9 | 10 | 11 | class Settings(dict): 12 | def __init__(self, path, *args, **kwds): 13 | self.path = path 14 | self.lock = threading.Lock() 15 | 16 | if os.path.exists(self.path): 17 | with open(path, 'r') as f: 18 | try: 19 | self.update(json.load(f)) 20 | except: 21 | logging.error('could not load settings.') 22 | dict.__init__(self, *args, **kwds) 23 | 24 | def __setitem__(self, key, value): 25 | dict.__setitem__(self, key, value) 26 | self.save() 27 | 28 | def __getitem__(self, key): 29 | try: 30 | return dict.__getitem__(self, key) or 0 31 | except KeyError: 32 | return 0 33 | 34 | def clear(self): 35 | with self.lock: 36 | dict.clear(self) 37 | self.save() 38 | 39 | def increment(self, key, value=1): 40 | with self.lock: 41 | self[key] += value 42 | 43 | def save(self): 44 | tmp = '%s_%s' % (self.path, time.time()) 45 | with open(tmp, 'w') as f: 46 | json.dump(self, f, separators=(',', ':')) 47 | os.replace(tmp, self.path) 48 | 49 | 50 | 51 | settings = Settings(SETTINGS_PATH) 52 | 53 | 54 | if False: 55 | print('Settings:') 56 | for k,v in sorted(settings.items()): print(' ', k, repr(v)) 57 | 58 | if __name__ == '__main__': 59 | # settings.clear() 60 | 61 | import random 62 | import logging 63 | logging.set_level(logging.DEBUG) 64 | logging.debug(settings) 65 | settings['abc'] = random.randint(0, 100) 66 | settings['xyz'] = random.randint(0, 100) 67 | logging.debug(settings) 68 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | from distutils import cmd 3 | from setuptools import setup, find_packages 4 | from setuptools.command.install import install 5 | from subprocess import check_call 6 | 7 | REQUIRED_PACKAGES = [ 8 | 'rumps', 9 | 'jinja2', 10 | 'dateparser', 11 | 'psutil', 12 | 'py2app', 13 | 'google-api-python-client', 14 | 'google-auth-httplib2', 15 | 'google-auth-oauthlib', 16 | 'PyDriller', 17 | 'pyinstaller', 18 | 'python-dateutil', 19 | 'elasticsearch', 20 | ] 21 | 22 | 23 | class CustomInstall(install): 24 | def run(self): 25 | def _post_install(): 26 | print("### Installing latest ElasticSearch using brew") 27 | check_call(['brew', 'tap', 'elastic/tap']) 28 | check_call(['brew', 'install', 'elastic/tap/elasticsearch-full']) 29 | check_call(['brew', 'install', 'elastic/tap/kibana-full']) 30 | for package in REQUIRED_PACKAGES: 31 | check_call(['python3', '-m', 'pip', 'install', package]) 32 | 33 | atexit.register(_post_install) 34 | install.run(self) 35 | 36 | 37 | setup( 38 | name = 'install', 39 | version = '0.1.0', 40 | packages = find_packages(), 41 | install_requires = REQUIRED_PACKAGES, 42 | cmdclass = { 43 | 'install': CustomInstall, 44 | }, 45 | ) -------------------------------------------------------------------------------- /simple_logging.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import inspect 3 | import os 4 | 5 | 6 | ERROR = 0 7 | WARNING = 1 8 | INFO = 2 9 | DEBUG = 3 10 | 11 | KINDS = ['ERROR', 'WARNING', 'INFO', 'DEBUG'] 12 | 13 | LINE = '=' * 120 14 | 15 | LOG_LEVEL = INFO 16 | 17 | 18 | def log_impl(msg): 19 | print(msg) 20 | 21 | 22 | def log(level, *msg): 23 | # type(int, str) -> None 24 | if level <= LOG_LEVEL: 25 | when = datetime.datetime.utcnow().strftime('%H:%M:%S') 26 | log_impl('%s %s %s: %s' % (caller_info(), when, KINDS[level], ' '.join(map(str,msg)))) 27 | 28 | 29 | def warning(*msg): 30 | # type(str) -> None 31 | log(WARNING, *msg) 32 | 33 | 34 | def error(*msg): 35 | # type(str) -> None 36 | log(ERROR, *msg) 37 | 38 | 39 | def info(*msg): 40 | # type(str) -> None 41 | log(INFO, *msg) 42 | 43 | 44 | def debug(*msg): 45 | # type(str) -> None 46 | log(DEBUG, *msg) 47 | 48 | 49 | def caller_info(skip=3): 50 | frame = inspect.stack()[skip][0] 51 | path = inspect.getfile(frame) 52 | base = os.path.dirname(__file__) 53 | return '%s:%d' % (path[len(base) + 1:], inspect.getlineno(frame)) 54 | 55 | 56 | def set_level(level): 57 | global LOG_LEVEL 58 | LOG_LEVEL = level 59 | 60 | 61 | def get_level(): 62 | return LOG_LEVEL 63 | 64 | 65 | if __name__ == '__main__': 66 | set_level(WARNING) 67 | warning('Tell the user about something potentially harmful.') 68 | 69 | set_level(DEBUG) 70 | debug('Show a highly verbose internal debugging message.') 71 | 72 | set_level(INFO) 73 | info('Report end-user level events.') 74 | info('Arg1', 'Arg2', 'Arg3', 1, 2, 3, True, '- Just some multiple args') 75 | 76 | set_level(ERROR) 77 | error('Something seriously wrong happened, execution probably ends now.') 78 | -------------------------------------------------------------------------------- /threadpool.py: -------------------------------------------------------------------------------- 1 | from queue import Queue 2 | from threading import Thread 3 | import traceback 4 | 5 | class Worker(Thread): 6 | """ Thread executing tasks from a given tasks queue """ 7 | def __init__(self, tasks): 8 | Thread.__init__(self) 9 | self.tasks = tasks 10 | self.daemon = True 11 | self.start() 12 | 13 | def run(self): 14 | while True: 15 | func, args = self.tasks.get() 16 | try: 17 | func(*args) 18 | except Exception as e: 19 | # An exception happened in this thread 20 | traceback.print_exc() 21 | finally: 22 | # Mark this task as done, whether an exception happened or not 23 | self.tasks.task_done() 24 | 25 | 26 | class ThreadPool: 27 | """ Pool of threads consuming tasks from a queue """ 28 | def __init__(self, task_count, tasks=None): 29 | # type (int,list) -> None 30 | self.tasks = Queue(task_count) 31 | for _ in range(task_count): 32 | Worker(self.tasks) 33 | if tasks: 34 | for task in tasks: 35 | self.add_task(*task) 36 | 37 | def add_task(self, func, *args): 38 | self.tasks.put((func, args)) 39 | 40 | def wait_completion(self): 41 | """ Wait for completion of all the tasks in the queue """ 42 | self.tasks.join() 43 | 44 | 45 | if __name__ == "__main__": 46 | import logging 47 | from time import sleep 48 | import time 49 | 50 | start = time.time() 51 | 52 | def wait_delay(d): 53 | logging.info("sleeping for (%d)sec" % d) 54 | sleep(d) 55 | 56 | pool = ThreadPool(15) 57 | pool.add_task(wait_delay, 1) 58 | pool.add_task(wait_delay, 3) 59 | pool.add_task(wait_delay, 2) 60 | pool.add_task(wait_delay, 5) 61 | pool.wait_completion() 62 | 63 | logging.info('The 4 tasks took %.1fs, not 11s' % (time.time() - start)) 64 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import psutil 4 | import sys 5 | import re 6 | import time 7 | 8 | 9 | CLEANUP_FILENAME_RE = re.compile(r'[~#%&*{}:<>?+|"]') 10 | INSTALL_FOLDER = getattr(sys, '_MEIPASS', os.path.dirname(__file__)) 11 | HOME_DIR = os.path.join(os.path.expanduser('~'), 'IKKE') 12 | HOME_DIR_SEGMENT_COUNT = len(HOME_DIR.split(os.path.pathsep)) 13 | ITEMS_DIR = os.path.join(HOME_DIR, 'items') 14 | FILE_DIR = os.path.join(ITEMS_DIR, 'file') 15 | CONTACT_DIR = os.path.join(ITEMS_DIR, 'contact') 16 | 17 | if not os.path.exists(HOME_DIR): 18 | os.mkdir(HOME_DIR) 19 | 20 | os.chdir(INSTALL_FOLDER) 21 | 22 | def get_timestamp(dt=None): 23 | try: 24 | return float(time.mktime(dt.timetuple())) 25 | except: 26 | return float(time.mktime(datetime.datetime.utcnow().timetuple())) 27 | 28 | KB = 1024 29 | MB = 1024 * KB 30 | GB = 1024 * MB 31 | TB = 1024 * GB 32 | 33 | def get_memory(): 34 | process = psutil.Process(os.getpid()) 35 | memory = process.memory_full_info() 36 | # rss vms shared text lib data dirty uss pss swap 37 | total = memory.rss 38 | if total < KB: return '%d' % total 39 | if total < MB: return '%.1dKB' % (total / KB) 40 | if total < GB: return '%.1dMB' % (total / MB) 41 | if total < TB: return '%.1dGB' % (total / GB) 42 | return '%d' % total 43 | 44 | def cleanup_filename(filename): 45 | return re.sub(CLEANUP_FILENAME_RE, '_', filename) 46 | 47 | --------------------------------------------------------------------------------