Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16be1c5ee4 | ||
|
|
7346a42227 | ||
|
|
85ed2e7227 | ||
|
|
f9f315da33 | ||
|
|
741aa78ff4 | ||
|
|
8e8838e6e9 | ||
|
|
889c09ad14 | ||
|
|
d11a7723f6 | ||
|
|
47051a66c2 | ||
|
|
61912960e2 | ||
|
|
9c5dd03473 | ||
|
|
9c6fee0fa6 | ||
|
|
8d55e8027a | ||
|
|
4583ce3be2 | ||
|
|
e8ed1db4d7 | ||
|
|
1b7f8ae9e4 | ||
|
|
b14160ec1f | ||
|
|
79e8ad3684 | ||
|
|
ff988d69a6 | ||
|
|
a49e2baa32 | ||
|
|
8161e6f2e6 | ||
|
|
9c861c238a | ||
|
|
53597f838c | ||
|
|
df39374970 | ||
|
|
0797190d41 | ||
|
|
7f58986f6d | ||
|
|
a4aa1076f0 | ||
|
|
c367e2b2b2 | ||
|
|
f742ad750e | ||
|
|
90c648ad56 | ||
|
|
39d8632827 | ||
|
|
e6c9b132e6 | ||
|
|
429853fea8 | ||
|
|
accbab69f1 | ||
|
|
20b1161782 | ||
|
|
88581689a1 | ||
|
|
f075ca17cb | ||
|
|
98371620f0 | ||
|
|
3e3ee37f74 | ||
|
|
1c24f91b32 | ||
|
|
0cfb8b53dc | ||
|
|
77fd696626 | ||
|
|
a69980a3f9 | ||
|
|
5fe072b2a9 | ||
|
|
379e81548d | ||
|
|
7c3cb92a34 | ||
|
|
a79ef66d90 | ||
|
|
c4e393e4e4 | ||
|
|
cdcf83ea27 | ||
|
|
758e2f7a03 | ||
|
|
1abc1377ea | ||
|
|
f49deb8509 | ||
|
|
ed9494f604 | ||
|
|
83b39fd8b7 | ||
|
|
e7d1d8a075 | ||
|
|
d9e83bfeec | ||
|
|
8e632775e6 | ||
|
|
270eb8c410 | ||
|
|
ac1aeaf554 | ||
|
|
b83eb15ffd | ||
|
|
1d1e4c28b2 | ||
|
|
09cb4f4aa1 | ||
|
|
680830c51f | ||
|
|
87e510a48f | ||
|
|
ce42a77162 | ||
|
|
f8b262cb1a | ||
|
|
924c208835 | ||
|
|
7c46ffa499 | ||
|
|
d9fe58de79 | ||
|
|
c8ad8ae383 | ||
|
|
83c1ffdcdc | ||
|
|
756972aa75 | ||
|
|
3603f714fb | ||
|
|
7507990ca5 | ||
|
|
1c8452be08 | ||
|
|
f535f5f9aa | ||
|
|
118b9c2c57 | ||
|
|
efd7bf4f1f | ||
|
|
8e0e7115c5 | ||
|
|
9abeb5e0ec | ||
|
|
fc15db46a3 | ||
|
|
435f4ed97e | ||
|
|
202f8e0ceb | ||
|
|
e16b6f16ff | ||
|
|
3b9e431d12 | ||
|
|
d49c884752 | ||
|
|
ac5ec3d6db | ||
|
|
93d921daa2 | ||
|
|
c44409fab8 | ||
|
|
d8035bdb12 | ||
|
|
d0b643c633 | ||
|
|
a0864cdca5 | ||
|
|
3c864ba511 | ||
|
|
1fe87263dc | ||
|
|
16228612c3 | ||
|
|
fbeb393963 | ||
|
|
0503c64f5f | ||
|
|
194208f772 |
@@ -8,6 +8,15 @@
|
||||
Document.svg
|
||||
</Icon>
|
||||
</Document>
|
||||
<Spreadsheet>
|
||||
<Attributes>
|
||||
<Attribute default="Spreadsheet" check="String" primary="True">Spreadsheet Name</Attribute>
|
||||
<Attribute default="DefaultFilePath" check="String" primary="False">File Path</Attribute>
|
||||
</Attributes>
|
||||
<Icon>
|
||||
Spreadsheet.svg
|
||||
</Icon>
|
||||
</Spreadsheet>
|
||||
<Image>
|
||||
<Attributes>
|
||||
<Attribute default="Image" check="String" primary="True">Image Name</Attribute>
|
||||
|
||||
@@ -15,9 +15,7 @@ class EntitiesDB:
|
||||
links on a project-wide scale.
|
||||
"""
|
||||
|
||||
def __init__(self, mainWindow, messageHandler, resourceHandler) -> None:
|
||||
self.messageHandler = messageHandler
|
||||
self.resourceHandler = resourceHandler
|
||||
def __init__(self, mainWindow) -> None:
|
||||
self.mainWindow = mainWindow
|
||||
self.dbLock = Lock()
|
||||
self.database = None
|
||||
@@ -33,17 +31,18 @@ class EntitiesDB:
|
||||
if self.database is not None:
|
||||
self.save()
|
||||
databaseFile = Path(self.mainWindow.SETTINGS.value("Project/FilesDir")).joinpath("LocalEntitiesDB.lsdb")
|
||||
self.messageHandler.debug(f'Opening Database at: {str(databaseFile)}')
|
||||
self.mainWindow.MESSAGEHANDLER.debug(f'Opening Database at: {databaseFile}')
|
||||
try:
|
||||
with open(databaseFile, "rb") as dbFile:
|
||||
self.database = self.mainWindow.RESOURCEHANDLER.reconstructGraphFullFromFile(load(dbFile))
|
||||
self.messageHandler.info('Loaded Local Entities Database.')
|
||||
self.mainWindow.MESSAGEHANDLER.info('Loaded Local Entities Database.')
|
||||
except FileNotFoundError:
|
||||
self.messageHandler.info('Creating new Local Entities Database.')
|
||||
self.mainWindow.MESSAGEHANDLER.info('Creating new Local Entities Database.')
|
||||
self.database = nx.DiGraph()
|
||||
except Exception as exc:
|
||||
self.messageHandler.error(f'Cannot parse Database: {str(exc)}\nCreating new Local Entities Database.',
|
||||
popUp=True)
|
||||
self.mainWindow.MESSAGEHANDLER.error(
|
||||
f'Cannot parse Database: {exc}\nCreating new Local Entities Database.',
|
||||
popUp=True)
|
||||
self.database = nx.DiGraph()
|
||||
|
||||
def resetTimeline(self) -> None:
|
||||
@@ -75,7 +74,7 @@ class EntitiesDB:
|
||||
with open(tmpSavePath, "wb") as dbFile:
|
||||
dump(self.mainWindow.RESOURCEHANDLER.deconstructGraphForFileDump(self.database), dbFile)
|
||||
move(tmpSavePath, databaseFile)
|
||||
self.messageHandler.info('Database Saved.')
|
||||
self.mainWindow.MESSAGEHANDLER.info('Database Saved.')
|
||||
|
||||
def addEntity(self, entJson: dict, fromServer: bool = False, updateTimeline: bool = True) -> Union[dict, None]:
|
||||
"""
|
||||
@@ -89,7 +88,7 @@ class EntitiesDB:
|
||||
if entJson.get('uid') is not None:
|
||||
exists = self.getEntityNoLock(entJson.get('uid'))
|
||||
|
||||
entity = self.resourceHandler.getEntityJson(
|
||||
entity = self.mainWindow.RESOURCEHANDLER.getEntityJson(
|
||||
entJson.get('Entity Type'),
|
||||
entJson)
|
||||
|
||||
@@ -124,7 +123,7 @@ class EntitiesDB:
|
||||
if entJson.get('uid') is not None:
|
||||
exists = self.getEntityNoLock(entJson.get('uid'))
|
||||
|
||||
entity = self.resourceHandler.getEntityJson(
|
||||
entity = self.mainWindow.RESOURCEHANDLER.getEntityJson(
|
||||
entJson.get('Entity Type'),
|
||||
entJson)
|
||||
|
||||
@@ -155,13 +154,12 @@ class EntitiesDB:
|
||||
"""
|
||||
with self.dbLock:
|
||||
exists = self.isLinkNoLock(linkJson['uid'])
|
||||
link = self.resourceHandler.getLinkJson(linkJson)
|
||||
link = self.mainWindow.RESOURCEHANDLER.getLinkJson(linkJson)
|
||||
if link is None:
|
||||
# This can technically be caused by a race condition if the user
|
||||
# either tries really hard or gets really unlucky.
|
||||
# Caused by deleting a node faster than the link can be created.
|
||||
self.messageHandler.error("Attempted to add Link with "
|
||||
"no uid to database.", popUp=True)
|
||||
self.mainWindow.MESSAGEHANDLER.error("Attempted to add Link with no uid to database.", popUp=True)
|
||||
return None
|
||||
else:
|
||||
linkUID = link['uid']
|
||||
@@ -203,7 +201,7 @@ class EntitiesDB:
|
||||
try:
|
||||
returnValue = self.database.nodes[uid]
|
||||
except KeyError:
|
||||
self.messageHandler.warning(f"Tried to get entity with nonexistent UID: {uid}")
|
||||
self.mainWindow.MESSAGEHANDLER.warning(f"Tried to get entity with nonexistent UID: {uid}")
|
||||
finally:
|
||||
return returnValue
|
||||
|
||||
@@ -215,8 +213,8 @@ class EntitiesDB:
|
||||
returnValue = None
|
||||
try:
|
||||
returnValue = [self.database.nodes[node] for node in self.database.nodes()]
|
||||
except KeyError:
|
||||
self.messageHandler.error("Tried to get entity with nonexistent UID.")
|
||||
except KeyError as keyError:
|
||||
self.mainWindow.MESSAGEHANDLER.error(f"Tried to get entity with nonexistent UID. Error: {keyError}")
|
||||
finally:
|
||||
return returnValue
|
||||
|
||||
@@ -230,7 +228,7 @@ class EntitiesDB:
|
||||
try:
|
||||
returnValue = [self.database.edges[edge] for edge in self.database.edges()]
|
||||
except KeyError:
|
||||
self.messageHandler.error("Tried to get link with nonexistent UID.")
|
||||
self.mainWindow.MESSAGEHANDLER.error("Tried to get link with nonexistent UID.")
|
||||
finally:
|
||||
return returnValue
|
||||
|
||||
@@ -258,8 +256,7 @@ class EntitiesDB:
|
||||
try:
|
||||
returnValue = self.database.edges[uid]
|
||||
except KeyError:
|
||||
self.messageHandler.error(
|
||||
"Tried to get link with nonexistent UID.")
|
||||
self.mainWindow.MESSAGEHANDLER.error(f"Tried to get link with nonexistent UID: {uid}")
|
||||
finally:
|
||||
return returnValue
|
||||
|
||||
@@ -310,7 +307,7 @@ class EntitiesDB:
|
||||
Checks if an entity with the specified primary attribute exists, and if it does, return it.
|
||||
"""
|
||||
result = None
|
||||
primaryField = self.resourceHandler.getPrimaryFieldForEntityType(entityType)
|
||||
primaryField = self.mainWindow.RESOURCEHANDLER.getPrimaryFieldForEntityType(entityType)
|
||||
if primaryField is None:
|
||||
return result
|
||||
with self.dbLock:
|
||||
|
||||
@@ -27,7 +27,7 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.exceptions import InvalidTag
|
||||
|
||||
# Amount of data to place in each message
|
||||
MESSAGE_DATA_SIZE = 8192
|
||||
MESSAGE_DATA_SIZE = 8192 * 5
|
||||
|
||||
# Needs to be a bit bigger than MESSAGE_DATA_SIZE
|
||||
RECV_SIZE = MESSAGE_DATA_SIZE + 1024
|
||||
@@ -54,8 +54,8 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
receive_start_collector_signal = QtCore.Signal(str, str, str, list, dict)
|
||||
receive_collector_result_signal = QtCore.Signal(str, str, str, list)
|
||||
receive_resolutions_signal = QtCore.Signal(dict)
|
||||
receive_completed_resolution_result_signal = QtCore.Signal(str, list)
|
||||
receive_completed_resolution_string_result_signal = QtCore.Signal(str, str)
|
||||
receive_completed_resolution_result_signal = QtCore.Signal(str, list, str)
|
||||
receive_completed_resolution_string_result_signal = QtCore.Signal(str, str, str)
|
||||
receive_document_summary_signal = QtCore.Signal(str, str)
|
||||
remove_server_resolution_from_running_signal = QtCore.Signal(str)
|
||||
receive_projects_list_signal = QtCore.Signal(list)
|
||||
@@ -149,12 +149,10 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
self.sock.send(passMessage)
|
||||
messageReceived = decrypter.update(self.sock.recv(RECV_SIZE)) + decrypter.finalize()
|
||||
if messageReceived == b"Passphrase is OK":
|
||||
self.threadInc = threading.Thread(target=self.scanIncoming)
|
||||
self.threadInc.setDaemon(True)
|
||||
self.threadInc = threading.Thread(target=self.scanIncoming, daemon=True)
|
||||
self.threadInc.start()
|
||||
|
||||
self.threadInb = threading.Thread(target=self.scanInbox)
|
||||
self.threadInb.setDaemon(True)
|
||||
self.threadInb = threading.Thread(target=self.scanInbox, daemon=True)
|
||||
self.threadInb.start()
|
||||
|
||||
return True
|
||||
@@ -270,7 +268,7 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
# Socket closed.
|
||||
break
|
||||
receivedInfo = oldData + receivedInfo
|
||||
messages = receivedInfo.split(b'\x03\x03\x03\x03\x03')
|
||||
messages = receivedInfo.split(b'\x03\x03\x03\x03\x03\x03\x03\x03')
|
||||
# Last message is either blank (i.e. '') or incomplete data, so we ignore it.
|
||||
oldData = messages[-1]
|
||||
messages = messages[:-1]
|
||||
@@ -364,9 +362,7 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
with contextlib.suppress(KeyError):
|
||||
dereferenced_entity = dict(entity)
|
||||
resolution_entities_to_send.append(dereferenced_entity)
|
||||
# Icon is not necessary for any resolution as of now: 2022/1/2.
|
||||
# Cutting it out saves data.
|
||||
dereferenced_entity['Icon'] = ''
|
||||
dereferenced_entity['Icon'] = dereferenced_entity['Icon'].toBase64().data()
|
||||
message = {'Operation': 'Run Resolution',
|
||||
'Arguments': {
|
||||
'resolution_name': resolution_name,
|
||||
@@ -379,9 +375,14 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
def receiveResolutionResult(self, resolution_name: str, resolution_result: Union[list, str],
|
||||
resolution_uid: str) -> None:
|
||||
if isinstance(resolution_result, str):
|
||||
self.receive_completed_resolution_string_result_signal.emit(resolution_name, resolution_result)
|
||||
self.receive_completed_resolution_string_result_signal.emit(resolution_name, resolution_result,
|
||||
resolution_uid)
|
||||
else:
|
||||
self.receive_completed_resolution_result_signal.emit(resolution_name, resolution_result)
|
||||
for res_result in resolution_result:
|
||||
if res_icon := res_result[0].get('Icon'):
|
||||
res_result[0]['Icon'] = QtCore.QByteArray(b64decode(res_icon))
|
||||
self.receive_completed_resolution_result_signal.emit(resolution_name, resolution_result,
|
||||
resolution_uid)
|
||||
self.remove_server_resolution_from_running_signal.emit(resolution_uid)
|
||||
|
||||
def abortResolution(self, resolution_name: str, resolution_uid: str) -> None:
|
||||
@@ -540,8 +541,8 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
:param filePath:
|
||||
:return:
|
||||
"""
|
||||
sendHelperThread = threading.Thread(target=self.sendFileHelper, args=(project_name, file_name, filePath))
|
||||
sendHelperThread.setDaemon(True)
|
||||
sendHelperThread = threading.Thread(target=self.sendFileHelper, daemon=True,
|
||||
args=(project_name, file_name, filePath))
|
||||
self.uploadingFiles[file_name] = sendHelperThread
|
||||
sendHelperThread.start()
|
||||
|
||||
@@ -557,7 +558,7 @@ class CommunicationsHandler(QtCore.QObject):
|
||||
if not filePath.exists() or not filePath.is_file():
|
||||
return
|
||||
with open(filePath, 'rb') as fileHandler:
|
||||
currThread = threading.currentThread()
|
||||
currThread = threading.current_thread()
|
||||
while getattr(currThread, "continue_running", True):
|
||||
filePart = fileHandler.read(512)
|
||||
if not filePart:
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import random
|
||||
|
||||
non_string_fields = ('Icon', 'Child UIDs')
|
||||
hidden_fields = ('uid', 'Date Last Edited', 'Child UIDs')
|
||||
hidden_fields = ('uid', 'Date Last Edited', 'Child UIDs', 'Canvas Banner', 'Entity Type')
|
||||
hidden_fields_dockbars = ('uid', 'Child UIDs', 'Canvas Banner', 'Icon')
|
||||
meta_fields = ('Child UIDs',)
|
||||
avoid_parsing_fields = ('uid', 'Date Last Edited', 'Child UIDs', 'Icon')
|
||||
avoid_parsing_fields = ('uid', 'Date Last Edited', 'Child UIDs', 'Icon', 'Canvas Banner')
|
||||
|
||||
# Closer to the top means more recent.
|
||||
user_agents = {'Chrome': {'Windows': ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import contextlib
|
||||
import re
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
@@ -12,16 +11,15 @@ import folium
|
||||
import networkx as nx
|
||||
from shutil import move
|
||||
from msgpack import dump, load
|
||||
from PIL import Image
|
||||
from PIL.ImageQt import ImageQt
|
||||
from pathlib import Path
|
||||
from PySide6 import QtWidgets, QtGui, QtCore
|
||||
from PySide6.QtWidgets import QGraphicsPixmapItem
|
||||
from PySide6.QtSvgWidgets import QGraphicsSvgItem
|
||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||
|
||||
from Core.Interface import Entity, Stylesheets
|
||||
from Core.ResourceHandler import RichNotesEditor
|
||||
from Core.Interface import Entity
|
||||
from Core.ResourceHandler import RichNotesEditor, resizePictureFromBuffer
|
||||
from Core.GlobalVariables import hidden_fields
|
||||
|
||||
|
||||
class WorkspaceWidget(QtWidgets.QWidget):
|
||||
@@ -90,7 +88,7 @@ class TabBar(QtWidgets.QTabBar):
|
||||
self.setMovable(True)
|
||||
|
||||
def mouseDoubleClickEvent(self, event) -> None:
|
||||
if event.button() != QtGui.Qt.LeftButton:
|
||||
if event.button() != QtGui.Qt.MouseButton.LeftButton:
|
||||
return
|
||||
currIndex = self.currentIndex()
|
||||
currName = self.tabText(currIndex)
|
||||
@@ -123,7 +121,6 @@ class RenameOrDeleteTabDialog(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, isSynced: bool, currName: str) -> None:
|
||||
super(RenameOrDeleteTabDialog, self).__init__()
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.setModal(True)
|
||||
self.setWindowTitle('Rename Or Delete Tab')
|
||||
|
||||
@@ -195,6 +192,9 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
|
||||
self.currentChanged.connect(self.currentTabChangedListener)
|
||||
|
||||
def getAllBanners(self) -> dict:
|
||||
return dict(self.mainWindow.RESOURCEHANDLER.banners)
|
||||
|
||||
def getCanvasDBPath(self):
|
||||
return Path(self.mainWindow.SETTINGS.value("Project/BaseDir")) / "Project Files" / "CanvasTabs.lscanvas"
|
||||
|
||||
@@ -352,7 +352,7 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
progress = QtWidgets.QProgressDialog(f'Resolving new nodes for resolution: {resolution_name}, please wait...',
|
||||
'Abort Resolving Nodes', 0, steps, self)
|
||||
|
||||
progress.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progress.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
progress.setMinimumDuration(1500)
|
||||
|
||||
# In case we have no entities in the database when the resolution finishes, i.e. the user deletes the origin
|
||||
@@ -600,8 +600,9 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
|
||||
# Save canvases
|
||||
with open(canvasDBPathTmp, "wb") as canvasDBFile:
|
||||
saveJson = {canvasName: [self.resourceHandler.deconstructGraphForFileDump(self.canvasTabs[canvasName].scene().sceneGraph),
|
||||
self.canvasTabs[canvasName].scene().scenePos] for canvasName in self.canvasTabs}
|
||||
saveJson = {canvasName: [self.resourceHandler.deconstructGraphForFileDump(
|
||||
self.canvasTabs[canvasName].scene().sceneGraph),
|
||||
self.canvasTabs[canvasName].scene().scenePos] for canvasName in self.canvasTabs}
|
||||
|
||||
dump(saveJson, canvasDBFile)
|
||||
move(canvasDBPathTmp, canvasDBPath)
|
||||
@@ -746,31 +747,25 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
self.name = name
|
||||
self.urlManager = urlManager
|
||||
|
||||
self.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
|
||||
self.setViewportUpdateMode(QtWidgets.QGraphicsView.FullViewportUpdate)
|
||||
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
|
||||
# self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
# self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
|
||||
self.setTransformationAnchor(QtWidgets.QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
||||
self.setViewportUpdateMode(QtWidgets.QGraphicsView.ViewportUpdateMode.FullViewportUpdate)
|
||||
self.setResizeAnchor(QtWidgets.QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
||||
self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(54, 69, 79)))
|
||||
self.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
self.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
|
||||
|
||||
self.setAcceptDrops(True)
|
||||
self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
|
||||
self.setDragMode(QtWidgets.QGraphicsView.DragMode.NoDrag)
|
||||
self.setSizePolicy(QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding))
|
||||
QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding))
|
||||
|
||||
self.dragOver = False
|
||||
self.synced = False
|
||||
|
||||
self.menu = QtWidgets.QMenu()
|
||||
self.menu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
selectMenu = self.menu.addMenu("Select...")
|
||||
selectMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
viewMenu = self.menu.addMenu("Hide / Delete...")
|
||||
viewMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
groupingMenu = self.menu.addMenu("Grouping...")
|
||||
groupingMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
bannersMenu = self.menu.addMenu("Banners...")
|
||||
|
||||
actionSelectChildren = QtGui.QAction('Select Child Nodes',
|
||||
selectMenu,
|
||||
@@ -835,6 +830,18 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
triggered=self.importConnectedEntities)
|
||||
self.menu.addAction(importConnectedEntitiesAction)
|
||||
|
||||
self.clearBannerMenu = QtGui.QAction('Clear Banners',
|
||||
bannersMenu,
|
||||
statusTip="Remove banners from the selected entities.",
|
||||
triggered=self.clearBanners)
|
||||
bannersMenu.addAction(self.clearBannerMenu)
|
||||
|
||||
self.setBannerIconMenu = QtGui.QAction('Set Banner Icon',
|
||||
bannersMenu,
|
||||
statusTip="Set a banner icon for the selected entities.",
|
||||
triggered=self.setBanners)
|
||||
bannersMenu.addAction(self.setBannerIconMenu)
|
||||
|
||||
def deleteItemsFromDatabase(self) -> None:
|
||||
items = self.scene().selectedItems()
|
||||
for item in items:
|
||||
@@ -853,7 +860,7 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
def drawBackground(self, painter: QtGui.QPainter, rect: Union[QtCore.QRectF, QtCore.QRect]) -> None:
|
||||
super(CanvasView, self).drawBackground(painter, rect)
|
||||
# Ensure that all links will always be drawn.
|
||||
[link.paint(painter, None, None) for link in list(self.scene().linksDict.values())]
|
||||
[link.paint(painter, None, None) for link in self.scene().linksDict.values()]
|
||||
|
||||
def centerViewportOnNode(self, uid: str) -> None:
|
||||
node = self.scene().getVisibleNodeForUID(uid)
|
||||
@@ -867,7 +874,6 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
def dragEnterEvent(self, event) -> None:
|
||||
if event.mimeData().hasText() or event.mimeData().hasImage():
|
||||
event.setAccepted(True)
|
||||
self.dragOver = True
|
||||
self.update()
|
||||
|
||||
def dragLeaveEvent(self, event) -> None:
|
||||
@@ -875,7 +881,6 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
|
||||
def dropEvent(self, event) -> None:
|
||||
pos = self.mapToScene(event.pos())
|
||||
self.dragOver = False
|
||||
mimeData = event.mimeData()
|
||||
jsonData = None
|
||||
|
||||
@@ -915,8 +920,7 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
if entityUID in self.scene().sceneGraph.nodes():
|
||||
wasGrouped = False
|
||||
for groupNode in [node for node in self.items() if isinstance(node, Entity.GroupNode)]:
|
||||
wasGrouped = \
|
||||
groupNode.removeSpecificItemFromGroupIfExists(entityUID)
|
||||
wasGrouped = groupNode.removeSpecificItemFromGroupIfExists(entityUID)
|
||||
if wasGrouped:
|
||||
self.removeGroupNodeLinksForUID(groupNode.uid, entityUID)
|
||||
groupNodeJson = self.tabbedPane.entityDB.getEntity(groupNode.uid)
|
||||
@@ -998,7 +1002,7 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
|
||||
def mouseReleaseEvent(self, event) -> None:
|
||||
QtWidgets.QGraphicsView.mouseReleaseEvent(self, event)
|
||||
self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
|
||||
self.setDragMode(QtWidgets.QGraphicsView.DragMode.NoDrag)
|
||||
itemsMoved = [item for item in self.scene().selectedItems()
|
||||
if isinstance(item, Entity.BaseNode)]
|
||||
for item in itemsMoved:
|
||||
@@ -1027,10 +1031,11 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
if event.button() == QtCore.Qt.MouseButton.RightButton and \
|
||||
not self.scene().linking and not self.scene().appendingToGroup:
|
||||
if len(self.scene().selectedItems()) == 0:
|
||||
self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)
|
||||
self.setDragMode(QtWidgets.QGraphicsView.DragMode.RubberBandDrag)
|
||||
else:
|
||||
items = self.scene().selectedItems()
|
||||
groupItems = [groupItem for groupItem in items if isinstance(groupItem, Entity.GroupNode)]
|
||||
entityItems = [entityItem for entityItem in items if isinstance(entityItem, Entity.BaseNode)]
|
||||
groupItems = [groupItem for groupItem in entityItems if isinstance(groupItem, Entity.GroupNode)]
|
||||
linkItems = [linkItem for linkItem in items if isinstance(linkItem, Entity.BaseConnector)]
|
||||
if groupItems:
|
||||
self.actionUngroup.setDisabled(False)
|
||||
@@ -1038,12 +1043,23 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
else:
|
||||
self.actionUngroup.setDisabled(True)
|
||||
self.actionUngroup.setEnabled(False)
|
||||
if len(items) > 1:
|
||||
self.actionGroup.setDisabled(False)
|
||||
self.actionGroup.setEnabled(True)
|
||||
if entityItems:
|
||||
self.clearBannerMenu.setDisabled(False)
|
||||
self.clearBannerMenu.setEnabled(True)
|
||||
self.setBannerIconMenu.setDisabled(False)
|
||||
self.setBannerIconMenu.setEnabled(True)
|
||||
|
||||
if len(entityItems) > 1:
|
||||
self.actionGroup.setDisabled(False)
|
||||
self.actionGroup.setEnabled(True)
|
||||
else:
|
||||
self.actionGroup.setDisabled(True)
|
||||
self.actionGroup.setEnabled(False)
|
||||
else:
|
||||
self.actionGroup.setDisabled(True)
|
||||
self.actionGroup.setEnabled(False)
|
||||
self.clearBannerMenu.setDisabled(True)
|
||||
self.clearBannerMenu.setEnabled(False)
|
||||
self.setBannerIconMenu.setDisabled(True)
|
||||
self.setBannerIconMenu.setEnabled(False)
|
||||
if linkItems:
|
||||
self.actionLinkDelete.setDisabled(False)
|
||||
self.actionLinkDelete.setEnabled(True)
|
||||
@@ -1055,7 +1071,7 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
elif event.button() == QtCore.Qt.MouseButton.RightButton and self.scene().appendingToGroup:
|
||||
self.scene().appendSelectedItemsToGroupToggle()
|
||||
elif event.button() == QtCore.Qt.MouseButton.LeftButton:
|
||||
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
|
||||
self.setDragMode(QtWidgets.QGraphicsView.DragMode.ScrollHandDrag)
|
||||
super(CanvasView, self).mousePressEvent(event)
|
||||
|
||||
def deleteSelectedLinks(self) -> None:
|
||||
@@ -1135,6 +1151,33 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
newNode.setSelected(True)
|
||||
self.scene().rearrangeGraph()
|
||||
|
||||
def clearBanners(self) -> None:
|
||||
selectedEntities = [item for item in self.scene().selectedItems() if isinstance(item, Entity.BaseNode)]
|
||||
for entity in selectedEntities:
|
||||
entityJson = self.tabbedPane.mainWindow.LENTDB.getEntity(entity.uid)
|
||||
entityJson['Canvas Banner'] = ''
|
||||
entity.updateBanner(True, None)
|
||||
self.tabbedPane.mainWindow.LENTDB.addEntity(entityJson, updateTimeline=False)
|
||||
|
||||
def setBanners(self) -> None:
|
||||
selectedEntities = [item for item in self.scene().selectedItems() if isinstance(item, Entity.BaseNode)]
|
||||
if not selectedEntities:
|
||||
self.tabbedPane.mainWindow.MESSAGEHANDLER.warning('Need to select at least one Entity to set its banner.',
|
||||
popUp=True)
|
||||
return
|
||||
bannerDialog = BannerSelector(self.tabbedPane.getAllBanners())
|
||||
if bannerDialog.exec():
|
||||
try:
|
||||
# This following line will throw IndexError if no banner is selected.
|
||||
selectedBannerItem = bannerDialog.bannerIconContainer.selectedItems()[0].text()
|
||||
self.scene().bannerDrawHelper(selectedEntities, selectedBannerItem)
|
||||
for entity in selectedEntities:
|
||||
entityJson = self.tabbedPane.mainWindow.LENTDB.getEntity(entity.uid)
|
||||
entityJson['Canvas Banner'] = selectedBannerItem
|
||||
self.tabbedPane.mainWindow.LENTDB.addEntity(entityJson, updateTimeline=False)
|
||||
except IndexError:
|
||||
self.tabbedPane.mainWindow.MESSAGEHANDLER.warning('No Banner selected.', popUp=True)
|
||||
|
||||
def takePictureOfView(self, justViewport: bool = True, transparentBackground: bool = False) -> QtGui.QImage:
|
||||
# Need to set size and format of pic before using it.
|
||||
# Ref: https://qtcentre.org/threads/10975-Help-Export-QGraphicsView-to-Image-File
|
||||
@@ -1144,7 +1187,7 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
for item in selectedItems:
|
||||
item.setSelected(False)
|
||||
if justViewport:
|
||||
picture = QtGui.QImage(self.viewport().size(), QtGui.QImage.Format_ARGB32_Premultiplied)
|
||||
picture = QtGui.QImage(self.viewport().size(), QtGui.QImage.Format.Format_ARGB32_Premultiplied)
|
||||
# Pictures are initialised with junk data - need to clear it out before painting
|
||||
# to avoid visual artifacts.
|
||||
picture.fill(QtGui.QColor(0, 0, 0, 0))
|
||||
@@ -1157,7 +1200,7 @@ class CanvasView(QtWidgets.QGraphicsView):
|
||||
else:
|
||||
# Convert QRectF to QRect - can't have floats when it comes to picture size.
|
||||
rectToPrint = self.scene().sceneRect().toRect()
|
||||
picture = QtGui.QImage(rectToPrint.size(), QtGui.QImage.Format_ARGB32_Premultiplied)
|
||||
picture = QtGui.QImage(rectToPrint.size(), QtGui.QImage.Format.Format_ARGB32_Premultiplied)
|
||||
# Pictures are initialised with junk data - need to clear it out before painting
|
||||
# to avoid visual artifacts.
|
||||
picture.fill(QtGui.QColor(0, 0, 0, 0))
|
||||
@@ -1224,6 +1267,49 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
# Re-Center the Label
|
||||
item.updateLabel(item.labelItem.text())
|
||||
|
||||
def bannerDrawHelper(self, entities: list, bannerName: str = None) -> None:
|
||||
"""
|
||||
If we are given a banner name, try to set each canvas entity banner to the banner with the given name.
|
||||
If not, then instead we get the banner that each entity is already assigned, and make sure it's drawn.
|
||||
"""
|
||||
if bannerName:
|
||||
try:
|
||||
bannerPathStr = self.parent().getAllBanners()[bannerName]
|
||||
with open(bannerPathStr, 'rb') as bannerFile:
|
||||
bannerByteArray = QtCore.QByteArray(bannerFile.read())
|
||||
for entity in entities:
|
||||
entity.updateBanner(False, bannerByteArray)
|
||||
except FileNotFoundError:
|
||||
self.parent().mainWindow.MESSAGEHANDLER.error(f'Banner Icon not found in filesystem: {bannerName}',
|
||||
popUp=True,
|
||||
exc_info=False)
|
||||
except KeyError:
|
||||
self.parent().mainWindow.MESSAGEHANDLER.warning(f'Invalid Banner: {bannerName}', popUp=True)
|
||||
else:
|
||||
notFoundBanners = set()
|
||||
for entity in entities:
|
||||
try:
|
||||
entityJson = self.parent().mainWindow.LENTDB.getEntity(entity.uid)
|
||||
if bannerPathStr := self.parent().getAllBanners().get(
|
||||
entityJson.get('Canvas Banner', ''), ''
|
||||
):
|
||||
with open(bannerPathStr, 'rb') as bannerFile:
|
||||
bannerByteArray = QtCore.QByteArray(bannerFile.read())
|
||||
entity.updateBanner(False, bannerByteArray)
|
||||
else:
|
||||
entity.updateBanner(True, None)
|
||||
except FileNotFoundError:
|
||||
if bannerName not in notFoundBanners:
|
||||
self.parent().mainWindow.MESSAGEHANDLER.error(f'Banner Icon not found in filesystem: '
|
||||
f'{bannerName}',
|
||||
popUp=True,
|
||||
exc_info=False)
|
||||
notFoundBanners.add(bannerName)
|
||||
except KeyError:
|
||||
if bannerName not in notFoundBanners:
|
||||
self.parent().mainWindow.MESSAGEHANDLER.warning(f'Invalid Banner: {bannerName}', popUp=True)
|
||||
notFoundBanners.add(bannerName)
|
||||
|
||||
# Redefined so that the BaseConnector items are not considered.
|
||||
def itemsBoundingRect(self) -> QtCore.QRectF:
|
||||
try:
|
||||
@@ -1246,6 +1332,7 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
def addNodeToScene(self, item, x=0, y=0) -> None:
|
||||
self.nodesDict[item.uid] = item
|
||||
self.addItem(item)
|
||||
self.bannerDrawHelper([item])
|
||||
item.setPos(QtCore.QPointF(x, y))
|
||||
self.parent().mainWindow.MESSAGEHANDLER.info(f'Added node: {str(item.uid)} | {item.labelItem.toPlainText()}')
|
||||
|
||||
@@ -1365,7 +1452,7 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
# Remove Cancel button from progress bar (user should not be able to stop canvas from loading).
|
||||
progress.setMinimumDuration(1500)
|
||||
progress.setCancelButton(None)
|
||||
progress.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progress.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
progressValue = 0
|
||||
|
||||
for node in sceneGraphNodes:
|
||||
@@ -1390,11 +1477,11 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
if groupItems is None:
|
||||
newNode = Entity.BaseNode(picture, node, nodePrimaryAttribute, self.entityTextFont,
|
||||
self.entityTextBrush)
|
||||
self.addNodeToScene(newNode)
|
||||
self.addNodeToScene(newNode, positions[node][0], positions[node][1])
|
||||
else:
|
||||
newNode = Entity.GroupNode(picture, node, nodePrimaryAttribute, self.entityTextFont,
|
||||
self.entityTextBrush)
|
||||
self.addNodeToScene(newNode)
|
||||
self.addNodeToScene(newNode, positions[node][0], positions[node][1])
|
||||
|
||||
newGroupList = newNode.listWidget
|
||||
newGroupListGraphic = self.addWidget(newGroupList)
|
||||
@@ -1403,8 +1490,6 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
for item in groupItems:
|
||||
self.sceneGraph.add_node(item, groupID=newNode.uid)
|
||||
|
||||
newNode.setPos(QtCore.QPointF(positions[node][0], positions[node][1]))
|
||||
|
||||
progressValue += 1
|
||||
progress.setValue(progressValue)
|
||||
|
||||
@@ -1515,7 +1600,7 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
# No triangulation library on Windows, so sfdp can't be used there.
|
||||
|
||||
if graphAlgorithm is None or graphAlgorithm not in ('sfdp', 'neato', 'dot', 'circular'):
|
||||
graphAlgorithm = self.parent().mainWindow.SETTINGS.value("Program/GraphLayout", 'dot')
|
||||
graphAlgorithm = self.parent().mainWindow.SETTINGS.value("Program/Graph Layout", 'dot')
|
||||
|
||||
# No real 'links' to group nodes by default (links to internal nodes don't count). This means that the
|
||||
# graph algorithms can create odd graphs where group nodes are concerned.
|
||||
@@ -1605,6 +1690,9 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
# Should never happen, but we will handle it if it does.
|
||||
self.parent().mainWindow.MESSAGEHANDLER.warning(f'Entity without valid Date Created: {str(node)}')
|
||||
entityDate = datetime.now().replace(microsecond=0, second=0)
|
||||
if entityDate.tzinfo is None or entityDate.tzinfo.utcoffset(entityDate) is None:
|
||||
# Make timezone-naive objects into timezone-aware, with the user's current timezone.
|
||||
entityDate = entityDate.replace(tzinfo=datetime.now().astimezone().tzinfo)
|
||||
if entityDate not in nodesOnCanvas:
|
||||
nodesOnCanvas[entityDate] = [node]
|
||||
else:
|
||||
@@ -1795,13 +1883,14 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
self.removeItem(item.iconItem)
|
||||
|
||||
pictureByteArray = pEditor.objectJson['Icon']
|
||||
item.pixmapItem = QtGui.QPixmap()
|
||||
item.pixmapItem.loadFromData(pictureByteArray)
|
||||
pictureByteArray = resizePictureFromBuffer(pictureByteArray, (40, 40))
|
||||
if pictureByteArray.data().startswith(b'<svg '):
|
||||
item.iconItem = QGraphicsSvgItem()
|
||||
item.iconItem.renderer().load(pictureByteArray)
|
||||
item.iconItem.setElementId("") # Force recalculation of geometry, else this looks like 1 pixel.
|
||||
else:
|
||||
item.pixmapItem = QtGui.QPixmap()
|
||||
item.pixmapItem.loadFromData(pictureByteArray)
|
||||
item.iconItem = QGraphicsPixmapItem(item.pixmapItem)
|
||||
|
||||
item.iconItem.setPos(item.pos())
|
||||
@@ -1832,9 +1921,16 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
for node in self.nodesDict:
|
||||
self.nodesDict[node].setSelected(True)
|
||||
|
||||
def selectChildNodes(self) -> None:
|
||||
items = [item.uid for item in self.selectedItems() if isinstance(item, Entity.BaseNode)]
|
||||
def selectNodesFromList(self, nodesList: list):
|
||||
self.clearSelection()
|
||||
for node in nodesList:
|
||||
if node in self.nodesDict:
|
||||
self.nodesDict[node].setSelected(True)
|
||||
|
||||
def selectChildNodes(self, clearSelection: bool = True) -> None:
|
||||
items = [item.uid for item in self.selectedItems() if isinstance(item, Entity.BaseNode)]
|
||||
if clearSelection:
|
||||
self.clearSelection()
|
||||
for item in items:
|
||||
childLinks = self.parent().entityDB.getOutgoingLinks(item)
|
||||
for childLink in childLinks:
|
||||
@@ -1842,9 +1938,10 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
with contextlib.suppress(KeyError):
|
||||
self.nodesDict[childLink[1]].setSelected(True)
|
||||
|
||||
def selectParentNodes(self) -> None:
|
||||
def selectParentNodes(self, clearSelection: bool = True) -> None:
|
||||
items = [item.uid for item in self.selectedItems() if isinstance(item, Entity.BaseNode)]
|
||||
self.clearSelection()
|
||||
if clearSelection:
|
||||
self.clearSelection()
|
||||
for item in items:
|
||||
parentLinks = self.parent().entityDB.getIncomingLinks(item)
|
||||
for parentLink in parentLinks:
|
||||
@@ -1971,14 +2068,13 @@ class PropertiesEditor(QtWidgets.QDialog):
|
||||
self.isEditingNode = isNode
|
||||
|
||||
self.setWindowTitle("Properties Editor")
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.setMinimumSize(500, 300)
|
||||
self.objectJson = objectJson
|
||||
self.canvas = canvas
|
||||
|
||||
self.itemProperties = QtWidgets.QFormLayout()
|
||||
for key in objectJson:
|
||||
if key in ('uid', 'Entity Type', 'Date Last Edited', 'Child UIDs'):
|
||||
if key in hidden_fields:
|
||||
continue
|
||||
keyField = QtWidgets.QLabel(key)
|
||||
if key == "Notes":
|
||||
@@ -1991,9 +2087,7 @@ class PropertiesEditor(QtWidgets.QDialog):
|
||||
valueField = QtWidgets.QLineEdit(str(objectJson[key]))
|
||||
self.itemProperties.addRow(keyField, valueField)
|
||||
acceptButton = QtWidgets.QPushButton("Confirm")
|
||||
acceptButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET)
|
||||
cancelButton = QtWidgets.QPushButton("Cancel")
|
||||
cancelButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET)
|
||||
acceptButton.setAutoDefault(True)
|
||||
acceptButton.setDefault(True)
|
||||
acceptButton.clicked.connect(self.accept)
|
||||
@@ -2005,16 +2099,16 @@ class PropertiesEditor(QtWidgets.QDialog):
|
||||
def accept(self):
|
||||
for row in range(self.itemProperties.rowCount()):
|
||||
key = self.itemProperties.itemAt(
|
||||
row, self.itemProperties.LabelRole).widget().text()
|
||||
row, self.itemProperties.ItemRole.LabelRole).widget().text()
|
||||
if key == "Notes":
|
||||
value = self.itemProperties.itemAt(
|
||||
row, self.itemProperties.FieldRole).widget().toMarkdown()
|
||||
row, self.itemProperties.ItemRole.FieldRole).widget().toMarkdown()
|
||||
elif key == 'Icon':
|
||||
value = self.itemProperties.itemAt(
|
||||
row, self.itemProperties.FieldRole).widget().pictureByteArray
|
||||
row, self.itemProperties.ItemRole.FieldRole).widget().pictureByteArray
|
||||
elif key == 'File Path':
|
||||
value = self.itemProperties.itemAt(
|
||||
row, self.itemProperties.FieldRole).widget().text()
|
||||
row, self.itemProperties.ItemRole.FieldRole).widget().text()
|
||||
projectFilesPath = Path(self.canvas.parent().mainWindow.SETTINGS.value("Project/FilesDir"))
|
||||
newPath = projectFilesPath / value
|
||||
if not newPath.is_relative_to(projectFilesPath):
|
||||
@@ -2024,7 +2118,7 @@ class PropertiesEditor(QtWidgets.QDialog):
|
||||
value = 'None'
|
||||
else:
|
||||
value = self.itemProperties.itemAt(
|
||||
row, self.itemProperties.FieldRole).widget().text()
|
||||
row, self.itemProperties.ItemRole.FieldRole).widget().text()
|
||||
# The last row is the Cancel / Accept buttons.
|
||||
if key != "Cancel":
|
||||
self.objectJson[key] = value
|
||||
@@ -2053,8 +2147,10 @@ class PropertiesEditorFilePathField(QtWidgets.QLineEdit):
|
||||
self.setText(str(value))
|
||||
|
||||
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
|
||||
selectedPath = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select File Path',
|
||||
options=QtWidgets.QFileDialog.DontUseNativeDialog)[0]
|
||||
selectedPath = QtWidgets.QFileDialog().getOpenFileName(
|
||||
parent=self,
|
||||
caption='Select File Path',
|
||||
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog)[0]
|
||||
if selectedPath != '':
|
||||
self.setText(str(Path(selectedPath).absolute()))
|
||||
|
||||
@@ -2067,50 +2163,25 @@ class PropertiesEditorIconField(QtWidgets.QLabel):
|
||||
super(PropertiesEditorIconField, self).__init__()
|
||||
self.pictureByteArray = pictureByteArray
|
||||
pixmapToSet = QtGui.QPixmap()
|
||||
pixmapToSet.loadFromData(pictureByteArray)
|
||||
pixmapToSet.loadFromData(resizePictureFromBuffer(pictureByteArray, (40, 40)))
|
||||
self.setPixmap(pixmapToSet)
|
||||
self.uid = uid
|
||||
self.canvas = canvas
|
||||
self.mainWindow = self.canvas.parent().mainWindow
|
||||
|
||||
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
|
||||
|
||||
selectedPath = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select New Icon',
|
||||
options=QtWidgets.QFileDialog.DontUseNativeDialog,
|
||||
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog,
|
||||
filter="Image Files (*.png *.jpg *.bmp *.svg)")[0]
|
||||
if selectedPath != '':
|
||||
try:
|
||||
filePath = Path(selectedPath)
|
||||
|
||||
with open(filePath, 'rb') as newIconFile:
|
||||
fileContents = newIconFile.read()
|
||||
if fileContents.startswith(b'<svg '):
|
||||
widthRegex = re.compile(b' width="\d*" ')
|
||||
for widthMatches in widthRegex.findall(fileContents):
|
||||
fileContents = fileContents.replace(widthMatches, b' ')
|
||||
heightRegex = re.compile(b' height="\d*" ')
|
||||
for heightMatches in heightRegex.findall(fileContents):
|
||||
fileContents = fileContents.replace(heightMatches, b' ')
|
||||
fileContents = fileContents.replace(b'<svg ', b'<svg height="40" width="40" ', 1)
|
||||
self.pictureByteArray = QtCore.QByteArray(fileContents)
|
||||
else:
|
||||
image = Image.open(selectedPath)
|
||||
thumbSize = 40, 40
|
||||
thumbnail = ImageQt(image.resize(thumbSize))
|
||||
self.pictureByteArray = QtCore.QByteArray()
|
||||
imageBuffer = QtCore.QBuffer(self.pictureByteArray)
|
||||
|
||||
imageBuffer.open(QtCore.QIODevice.WriteOnly)
|
||||
|
||||
thumbnail.save(imageBuffer, "PNG")
|
||||
imageBuffer.close()
|
||||
|
||||
filePath = Path(selectedPath)
|
||||
newPic = self.mainWindow.RESOURCEHANDLER.getPictureFromFile(filePath)
|
||||
if newPic is not None:
|
||||
self.pictureByteArray = newPic
|
||||
pixmapToSet = QtGui.QPixmap()
|
||||
pixmapToSet.loadFromData(self.pictureByteArray)
|
||||
pixmapToSet.loadFromData(resizePictureFromBuffer(newPic, (40, 40)))
|
||||
self.setPixmap(pixmapToSet)
|
||||
except ValueError as ve:
|
||||
# Image type is unsupported (for ImageQt)
|
||||
# Supported types: 1, L, P, RGB, RGBA
|
||||
self.canvas.parent().mainWindow.MESSAGEHANDLER.warning(f'Invalid Image selected: {str(ve)}', popUp=True)
|
||||
|
||||
super(PropertiesEditorIconField, self).mousePressEvent(event)
|
||||
|
||||
@@ -2119,7 +2190,6 @@ class SendToOtherTabCanvasSelector(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, canvasNames: list):
|
||||
super(SendToOtherTabCanvasSelector, self).__init__()
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.setModal(True)
|
||||
self.setWindowTitle('Move Selected Entities to New Canvas')
|
||||
|
||||
@@ -2143,3 +2213,48 @@ class SendToOtherTabCanvasSelector(QtWidgets.QDialog):
|
||||
sendToOtherCanvasLayout.addWidget(self.canvasNameSelector, 1, 1, 1, 2)
|
||||
sendToOtherCanvasLayout.addWidget(cancelButton, 2, 0, 1, 1)
|
||||
sendToOtherCanvasLayout.addWidget(confirmButton, 2, 1, 1, 2)
|
||||
|
||||
|
||||
class BannerSelector(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, bannerDict: dict):
|
||||
super(BannerSelector, self).__init__()
|
||||
self.setModal(True)
|
||||
self.setWindowTitle('Select Banner')
|
||||
|
||||
bannerLayout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(bannerLayout)
|
||||
|
||||
descriptionLabel = QtWidgets.QLabel("Select the Banner that you want to apply to the selected Entities:")
|
||||
descriptionLabel.setWordWrap(True)
|
||||
bannerLayout.addWidget(descriptionLabel)
|
||||
|
||||
self.bannerIconContainer = QtWidgets.QListWidget()
|
||||
self.bannerIconContainer.setFlow(self.bannerIconContainer.Flow.LeftToRight)
|
||||
self.bannerIconContainer.setMovement(self.bannerIconContainer.Movement.Static)
|
||||
self.bannerIconContainer.setViewMode(self.bannerIconContainer.ViewMode.IconMode)
|
||||
self.bannerIconContainer.setLayoutMode(self.bannerIconContainer.LayoutMode.SinglePass)
|
||||
self.bannerIconContainer.setSelectionMode(self.bannerIconContainer.SelectionMode.SingleSelection)
|
||||
self.bannerIconContainer.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.bannerIconContainer.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.bannerIconContainer.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum,
|
||||
QtWidgets.QSizePolicy.Policy.Minimum)
|
||||
|
||||
for bannerID, bannerPathStr in bannerDict.items():
|
||||
bannerPixmap = QtGui.QIcon(bannerPathStr)
|
||||
QtWidgets.QListWidgetItem(bannerPixmap, bannerID, self.bannerIconContainer)
|
||||
|
||||
bannerLayout.addWidget(self.bannerIconContainer)
|
||||
|
||||
buttonsWidget = QtWidgets.QWidget()
|
||||
buttonsWidgetLayout = QtWidgets.QHBoxLayout()
|
||||
buttonsWidget.setLayout(buttonsWidgetLayout)
|
||||
cancelButton = QtWidgets.QPushButton('Cancel')
|
||||
cancelButton.clicked.connect(self.reject)
|
||||
acceptButton = QtWidgets.QPushButton('Confirm')
|
||||
acceptButton.clicked.connect(self.accept)
|
||||
acceptButton.setAutoDefault(True)
|
||||
acceptButton.setDefault(True)
|
||||
buttonsWidgetLayout.addWidget(cancelButton)
|
||||
buttonsWidgetLayout.addWidget(acceptButton)
|
||||
bannerLayout.addWidget(buttonsWidget)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
from json import dumps
|
||||
from Core.Interface.Entity import BaseNode
|
||||
from Core.Interface import Stylesheets
|
||||
from PySide6 import QtWidgets, QtCore, QtGui
|
||||
|
||||
from Core.ResourceHandler import resizePictureFromBuffer
|
||||
|
||||
|
||||
class DockBarOne(QtWidgets.QDockWidget):
|
||||
|
||||
@@ -36,11 +37,11 @@ class DockBarOne(QtWidgets.QDockWidget):
|
||||
self.resolutionManager = resolutionManager
|
||||
self.resourceHandler = resourceHandler
|
||||
self.lentDB = entityDatabase
|
||||
self.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea |
|
||||
QtCore.Qt.RightDockWidgetArea)
|
||||
self.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
|
||||
QtWidgets.QDockWidget.DockWidgetFloatable |
|
||||
QtWidgets.QDockWidget.DockWidgetClosable)
|
||||
self.setAllowedAreas(QtCore.Qt.DockWidgetArea.LeftDockWidgetArea |
|
||||
QtCore.Qt.DockWidgetArea.RightDockWidgetArea)
|
||||
self.setFeatures(QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetMovable |
|
||||
QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetFloatable |
|
||||
QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetClosable)
|
||||
self.setWindowTitle(title)
|
||||
self.setObjectName(title)
|
||||
|
||||
@@ -73,14 +74,13 @@ class EntityList(QtWidgets.QTreeWidget):
|
||||
def __init__(self, entityDB, mainWindow, parent=None):
|
||||
super(EntityList, self).__init__(parent=parent)
|
||||
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.entityDB = entityDB
|
||||
self.mainWindow = mainWindow
|
||||
self.setDragEnabled(True)
|
||||
self.setHeaderLabels(['Entity List'])
|
||||
self.setAlternatingRowColors(False)
|
||||
self.setMinimumWidth(200)
|
||||
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
self.menu = QtWidgets.QMenu()
|
||||
|
||||
actionDelete = QtGui.QAction('Delete Selected Items',
|
||||
@@ -95,8 +95,6 @@ class EntityList(QtWidgets.QTreeWidget):
|
||||
triggered=self.addItemsToCurrentCanvas)
|
||||
self.menu.addAction(actionAddToCurrentCanvas)
|
||||
|
||||
self.menu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
|
||||
self.entityCategories: dict = {}
|
||||
self.entityTypes: dict = {}
|
||||
self.loadEntities()
|
||||
@@ -120,7 +118,8 @@ class EntityList(QtWidgets.QTreeWidget):
|
||||
topItem = self.entityTypes[entity['Entity Type']]
|
||||
primaryAttr = entity[list(entity)[1]]
|
||||
pixmapIcon = QtGui.QPixmap()
|
||||
pixmapIcon.loadFromData(entity['Icon'])
|
||||
resizedIcon = resizePictureFromBuffer(entity['Icon'], (40, 40))
|
||||
pixmapIcon.loadFromData(resizedIcon)
|
||||
EntityWidget(topItem,
|
||||
entity['uid'],
|
||||
QtGui.QIcon(pixmapIcon),
|
||||
@@ -146,7 +145,8 @@ class EntityList(QtWidgets.QTreeWidget):
|
||||
child.setText(0, primaryAttr)
|
||||
return
|
||||
pixmapIcon = QtGui.QPixmap()
|
||||
pixmapIcon.loadFromData(entityJson['Icon'])
|
||||
resizedIcon = resizePictureFromBuffer(entityJson['Icon'], (40, 40))
|
||||
pixmapIcon.loadFromData(resizedIcon)
|
||||
EntityWidget(entityTypeItem,
|
||||
entityJson['uid'],
|
||||
QtGui.QIcon(pixmapIcon),
|
||||
@@ -178,7 +178,10 @@ class EntityList(QtWidgets.QTreeWidget):
|
||||
"""
|
||||
Handle dragging of entities onto canvas.
|
||||
"""
|
||||
itemDragged = self.itemAt(event.pos())
|
||||
if event.buttons() == QtCore.Qt.MouseButton.LeftButton:
|
||||
itemDragged = self.itemAt(event.pos())
|
||||
else:
|
||||
return
|
||||
|
||||
# Categories & entity names don't have uids.
|
||||
try:
|
||||
@@ -201,7 +204,6 @@ class EntityList(QtWidgets.QTreeWidget):
|
||||
drag.setPixmap(pixmap)
|
||||
drag.setHotSpot(QtCore.QPoint(pixmap.rect().width() // 2, pixmap.rect().height() // 2))
|
||||
drag.exec_()
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
|
||||
super(EntityList, self).mousePressEvent(event)
|
||||
@@ -244,13 +246,12 @@ class DocList(QtWidgets.QTreeWidget):
|
||||
def __init__(self, resourceHandler, parent=None) -> None:
|
||||
super(DocList, self).__init__(parent=parent)
|
||||
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.resourceHandler = resourceHandler
|
||||
self.setAlternatingRowColors(False)
|
||||
self.setHeaderLabels(['Files Loaded'])
|
||||
self.uploadingFileWidgets = []
|
||||
self.uploadedFileWidgets = []
|
||||
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
|
||||
def addUploadingFileToList(self, fileName: str) -> None:
|
||||
newWidget = DocWidget(self,
|
||||
@@ -301,7 +302,6 @@ class ResolutionList(QtWidgets.QTreeWidget):
|
||||
|
||||
super(ResolutionList, self).__init__(parent=parent)
|
||||
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.resolutionManager = resolutionManager
|
||||
self.lentDB = entityDatabase
|
||||
self.mainWindow = mainWindow
|
||||
@@ -310,7 +310,7 @@ class ResolutionList(QtWidgets.QTreeWidget):
|
||||
self.setAlternatingRowColors(False)
|
||||
self.setMinimumWidth(200)
|
||||
self.setSortingEnabled(True)
|
||||
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
|
||||
|
||||
self.loadAllResolutions()
|
||||
|
||||
@@ -368,13 +368,12 @@ class NodeList(QtWidgets.QTreeWidget):
|
||||
def __init__(self, resourceHandler, parent=None) -> None:
|
||||
super(NodeList, self).__init__(parent=parent)
|
||||
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.resourceHandler = resourceHandler
|
||||
self.setDragEnabled(True)
|
||||
self.setHeaderLabels(['Entities'])
|
||||
self.setAlternatingRowColors(False)
|
||||
self.setSortingEnabled(True)
|
||||
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
self.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
|
||||
self.allEntities = []
|
||||
|
||||
self.loadEntities()
|
||||
@@ -398,8 +397,7 @@ class NodeList(QtWidgets.QTreeWidget):
|
||||
"""
|
||||
Handle dragging of entities onto canvas.
|
||||
"""
|
||||
# No, I have no idea why this is the case: v
|
||||
if event.button() == QtGui.Qt.MouseButton.NoButton:
|
||||
if event.buttons() == QtCore.Qt.MouseButton.LeftButton:
|
||||
itemDragged = self.itemAt(event.pos())
|
||||
if itemDragged is None or \
|
||||
itemDragged.text(0) not in self.allEntities:
|
||||
@@ -419,9 +417,6 @@ class NodeList(QtWidgets.QTreeWidget):
|
||||
drag.setPixmap(pixmap)
|
||||
drag.setHotSpot(QtCore.QPoint(pixmap.rect().width() // 2, pixmap.rect().height() // 2))
|
||||
drag.exec_()
|
||||
else:
|
||||
# This should never happen.
|
||||
super(NodeList, self).mousePressEvent(event)
|
||||
|
||||
|
||||
class NodeWidget(QtWidgets.QTreeWidgetItem):
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import contextlib
|
||||
from PySide6 import QtWidgets, QtCore, QtCharts, QtGui
|
||||
from Core.Interface import Stylesheets
|
||||
from datetime import datetime
|
||||
from getpass import getuser
|
||||
import networkx as nx
|
||||
@@ -15,7 +14,6 @@ class DockBarThree(QtWidgets.QDockWidget):
|
||||
"""
|
||||
|
||||
def initialiseLayout(self):
|
||||
# self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
childWidget = QtWidgets.QWidget()
|
||||
childWidget.setLayout(QtWidgets.QVBoxLayout())
|
||||
childWidget.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -30,20 +28,18 @@ class DockBarThree(QtWidgets.QDockWidget):
|
||||
self.tabPane.addTab(self.logViewer, 'Program Log')
|
||||
self.tabPane.addTab(self.timeWidget, 'Timeline')
|
||||
childWidget2.layout().addWidget(self.chatBox)
|
||||
self.serverStatus.setStyleSheet(Stylesheets.DOCK_BAR_LABEL)
|
||||
childWidget.layout().addWidget(self.serverStatus)
|
||||
|
||||
def __init__(self, mainWindow, title="Dockbar Three"):
|
||||
super(DockBarThree, self).__init__(parent=mainWindow)
|
||||
self.setAllowedAreas(QtCore.Qt.TopDockWidgetArea |
|
||||
QtCore.Qt.BottomDockWidgetArea)
|
||||
self.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
|
||||
QtWidgets.QDockWidget.DockWidgetFloatable |
|
||||
QtWidgets.QDockWidget.DockWidgetClosable)
|
||||
self.setAllowedAreas(QtCore.Qt.DockWidgetArea.TopDockWidgetArea |
|
||||
QtCore.Qt.DockWidgetArea.BottomDockWidgetArea)
|
||||
self.setFeatures(QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetMovable |
|
||||
QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetFloatable |
|
||||
QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetClosable)
|
||||
self.setWindowTitle(title)
|
||||
self.setObjectName(title)
|
||||
self.setMaximumHeight(275)
|
||||
self.setMinimumHeight(275)
|
||||
self.setMinimumHeight(300)
|
||||
|
||||
self.tabPane = QtWidgets.QTabWidget()
|
||||
|
||||
@@ -51,7 +47,6 @@ class DockBarThree(QtWidgets.QDockWidget):
|
||||
self.chatBox = ChatBox(self, self.parent())
|
||||
self.timeWidget = TimeWidget(self, self.parent())
|
||||
self.logViewer = QtWidgets.QPlainTextEdit()
|
||||
self.logViewer.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
|
||||
# Because we're not going to stop the thread before closing, an error will be thrown by Qt.
|
||||
# That error can be safely ignored.
|
||||
@@ -77,11 +72,11 @@ class TimeWidget(QtWidgets.QWidget):
|
||||
|
||||
self.timelineChart = QtCharts.QChart()
|
||||
self.timelineChart.setTitle("Timeline")
|
||||
self.timelineChart.setTheme(QtCharts.QChart.ChartThemeBlueCerulean)
|
||||
self.timelineChart.setTheme(QtCharts.QChart.ChartTheme.ChartThemeBlueCerulean)
|
||||
self.timelineChart.setMargins(QtCore.QMargins(0, 0, 0, 0))
|
||||
self.chartView = QtCharts.QChartView(self.timelineChart)
|
||||
self.chartView.setRubberBand(QtCharts.QChartView.NoRubberBand)
|
||||
self.timelineChart.setAnimationOptions(QtCharts.QChart.AllAnimations)
|
||||
self.chartView.setRubberBand(QtCharts.QChartView.RubberBand.NoRubberBand)
|
||||
self.timelineChart.setAnimationOptions(QtCharts.QChart.AnimationOption.AllAnimations)
|
||||
self.timelineChart.setAnimationDuration(250)
|
||||
self.timelineChart.legend().hide()
|
||||
|
||||
@@ -96,7 +91,7 @@ class TimeWidget(QtWidgets.QWidget):
|
||||
# Ref: https://qtcentre.org/threads/10975-Help-Export-QGraphicsView-to-Image-File
|
||||
# Rendering best optimized to rgb32 and argb32_premultiplied.
|
||||
# Ref: https://doc.qt.io/qtforpython/PySide6/QtGui/QImage.html?highlight=qimage#image-formats
|
||||
picture = QtGui.QImage(self.chartView.size(), QtGui.QImage.Format_ARGB32_Premultiplied)
|
||||
picture = QtGui.QImage(self.chartView.size(), QtGui.QImage.Format.Format_ARGB32_Premultiplied)
|
||||
# Pictures are initialised with junk data - need to clear it out before painting
|
||||
# to avoid visual artifacts.
|
||||
picture.fill(QtGui.QColor(0, 0, 0, 0))
|
||||
@@ -222,7 +217,7 @@ class TimeWidget(QtWidgets.QWidget):
|
||||
xAxisValues = []
|
||||
|
||||
barSet = TimelineBarSet('Entities', self, timestep, list(barsDict))
|
||||
barSet.setColor(QtGui.Qt.darkCyan)
|
||||
barSet.setColor(QtGui.Qt.GlobalColor.darkCyan)
|
||||
for bar in barsDict:
|
||||
barSet.append(barsDict[bar])
|
||||
timelineSeries.append(barSet)
|
||||
@@ -289,35 +284,33 @@ class TimelineTimescaleSelector(QtWidgets.QLabel):
|
||||
|
||||
def __init__(self, timeWidget: TimeWidget):
|
||||
super(TimelineTimescaleSelector, self).__init__(parent=timeWidget)
|
||||
self.setStyleSheet("""border: 2px solid rgb(44, 49, 58);""")
|
||||
|
||||
self.timeWidget = timeWidget
|
||||
self.setMinimumWidth(150)
|
||||
self.setMaximumHeight(150)
|
||||
|
||||
self.setLayout(QtWidgets.QFormLayout())
|
||||
self.setFrameStyle(QtWidgets.QFrame.Sunken)
|
||||
self.setFrameStyle(QtWidgets.QFrame.Shadow.Sunken)
|
||||
|
||||
self.yearButton = QtWidgets.QPushButton(' Year: ')
|
||||
self.yearButton.clicked.connect(self.yearButtonPressed)
|
||||
self.yearText = QtWidgets.QLabel('-')
|
||||
self.yearText.setFrameStyle(QtWidgets.QFrame.Sunken)
|
||||
self.yearText.setFrameStyle(QtWidgets.QFrame.Shadow.Sunken)
|
||||
self.monthButton = QtWidgets.QPushButton(' Month: ')
|
||||
self.monthButton.clicked.connect(self.monthButtonPressed)
|
||||
self.monthText = QtWidgets.QLabel('X')
|
||||
self.monthText.setFrameStyle(QtWidgets.QFrame.Sunken)
|
||||
self.monthText.setFrameStyle(QtWidgets.QFrame.Shadow.Sunken)
|
||||
self.dayButton = QtWidgets.QPushButton(' Day: ')
|
||||
self.dayButton.clicked.connect(self.dayButtonPressed)
|
||||
self.dayText = QtWidgets.QLabel('X')
|
||||
self.dayText.setFrameStyle(QtWidgets.QFrame.Sunken)
|
||||
self.dayText.setFrameStyle(QtWidgets.QFrame.Shadow.Sunken)
|
||||
self.hourButton = QtWidgets.QPushButton(' Hour: ')
|
||||
self.hourButton.clicked.connect(self.hourButtonPressed)
|
||||
self.hourText = QtWidgets.QLabel('X')
|
||||
self.hourText.setFrameStyle(QtWidgets.QFrame.Sunken)
|
||||
self.hourText.setFrameStyle(QtWidgets.QFrame.Shadow.Sunken)
|
||||
self.minuteButton = QtWidgets.QPushButton(' Minute: ')
|
||||
self.minuteButton.clicked.connect(self.minuteButtonPressed)
|
||||
self.minuteText = QtWidgets.QLabel('X')
|
||||
self.minuteText.setFrameStyle(QtWidgets.QFrame.Sunken)
|
||||
self.minuteText.setFrameStyle(QtWidgets.QFrame.Shadow.Sunken)
|
||||
|
||||
self.layout().addRow(self.yearButton, self.yearText)
|
||||
self.layout().addRow(self.monthButton, self.monthText)
|
||||
@@ -462,8 +455,8 @@ class ServerStatusBox(QtWidgets.QLabel):
|
||||
|
||||
def __init__(self, parent):
|
||||
super(ServerStatusBox, self).__init__(parent=parent)
|
||||
self.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
|
||||
self.setFrameStyle(QtWidgets.QFrame.Sunken | QtWidgets.QFrame.StyledPanel)
|
||||
self.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.setFrameStyle(QtWidgets.QFrame.Shadow.Sunken | QtWidgets.QFrame.Shape.StyledPanel)
|
||||
self.setText("Not connected to a server")
|
||||
|
||||
def updateStatus(self, status: str):
|
||||
@@ -491,8 +484,7 @@ class ChatBox(QtWidgets.QWidget):
|
||||
self.setLayout(chatLayout)
|
||||
|
||||
chatLabel = QtWidgets.QLabel('Project Collaboration Chat')
|
||||
chatLabel.setStyleSheet(Stylesheets.DOCK_BAR_LABEL)
|
||||
chatLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
chatLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.textView = QtWidgets.QPlainTextEdit()
|
||||
self.textView.setReadOnly(True)
|
||||
self.textSendBox = QtWidgets.QLineEdit()
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
from pathlib import Path
|
||||
import magic
|
||||
from PySide6 import QtWidgets, QtCore, QtGui
|
||||
from Core.Interface import Stylesheets
|
||||
from Core.ResourceHandler import MinSizeStackedLayout, RichNotesEditor
|
||||
from Core.ResourceHandler import MinSizeStackedLayout, RichNotesEditor, resizePictureFromBuffer
|
||||
from Core.GlobalVariables import hidden_fields_dockbars
|
||||
|
||||
|
||||
class DockBarTwo(QtWidgets.QDockWidget):
|
||||
@@ -15,9 +15,9 @@ class DockBarTwo(QtWidgets.QDockWidget):
|
||||
scrollAreaWidget = QtWidgets.QScrollArea()
|
||||
|
||||
scrollAreaWidget.setWidget(self.entDetails)
|
||||
scrollAreaWidget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
||||
scrollAreaWidget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
scrollAreaWidget.setWidgetResizable(True)
|
||||
scrollAreaWidget.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
|
||||
scrollAreaWidget.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum)
|
||||
|
||||
childWidget.addTab(scrollAreaWidget, 'Entity Details')
|
||||
childWidget.addTab(self.oracle, 'Oracle')
|
||||
@@ -30,11 +30,11 @@ class DockBarTwo(QtWidgets.QDockWidget):
|
||||
title="DockBar Two"):
|
||||
super(DockBarTwo, self).__init__(parent=mainWindow)
|
||||
|
||||
self.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea |
|
||||
QtCore.Qt.RightDockWidgetArea)
|
||||
self.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
|
||||
QtWidgets.QDockWidget.DockWidgetFloatable |
|
||||
QtWidgets.QDockWidget.DockWidgetClosable)
|
||||
self.setAllowedAreas(QtCore.Qt.DockWidgetArea.LeftDockWidgetArea |
|
||||
QtCore.Qt.DockWidgetArea.RightDockWidgetArea)
|
||||
self.setFeatures(QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetMovable |
|
||||
QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetFloatable |
|
||||
QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetClosable)
|
||||
self.setWindowTitle(title)
|
||||
self.resourceHandler = resourceHandler
|
||||
self.entityDB = entityDB
|
||||
@@ -98,7 +98,7 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
self.entityDB = entityDB
|
||||
self.detailsLayout = MinSizeStackedLayout()
|
||||
self.setLayout(self.detailsLayout)
|
||||
self.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
|
||||
self.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum)
|
||||
|
||||
layoutNothingSelected = QtWidgets.QVBoxLayout()
|
||||
widgetNothing = QtWidgets.QWidget()
|
||||
@@ -118,7 +118,7 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
# Need to keep track of how many nodes are selected.
|
||||
# ~ Nothing Selected/Hovered Layout
|
||||
nothingLabel = QtWidgets.QLabel("Nothing is Selected.")
|
||||
nothingLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
nothingLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
layoutNothingSelected.addWidget(nothingLabel)
|
||||
###
|
||||
|
||||
@@ -128,10 +128,12 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
summaryPanel = QtWidgets.QWidget()
|
||||
summaryPanel.setLayout(summaryLayout)
|
||||
self.summaryIcon = QtWidgets.QLabel("")
|
||||
self.summaryIcon.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
|
||||
self.summaryIcon.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum)
|
||||
self.entityTypeLabel = QtWidgets.QLabel("")
|
||||
self.entityUIDLabel = QtWidgets.QLabel("")
|
||||
self.entityPrimaryLabel = QtWidgets.QLabel("")
|
||||
self.entityUIDLabel = QtWidgets.QLineEdit("")
|
||||
self.entityUIDLabel.setReadOnly(True)
|
||||
self.entityPrimaryLabel = QtWidgets.QLineEdit("")
|
||||
self.entityPrimaryLabel.setReadOnly(True)
|
||||
summaryLayout.addWidget(self.summaryIcon, 0, 0)
|
||||
summaryLayout.addWidget(self.entityTypeLabel, 0, 1, 1, 3)
|
||||
summaryLayout.addWidget(self.entityPrimaryLabel, 1, 1)
|
||||
@@ -164,13 +166,10 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
oneLinkRelPanel.setMaximumHeight(150)
|
||||
oneLinkRelPanel.setLayout(oneLinkRelLayout)
|
||||
self.linkParent = SingleLinkItem(self, mainWindow)
|
||||
self.linkParent.setStyleSheet(Stylesheets.DOCK_BAR_TWO_LINK)
|
||||
self.linkIcon = QtWidgets.QLabel("")
|
||||
self.linkIcon.setMaximumHeight(90)
|
||||
self.linkIcon.setStyleSheet(Stylesheets.DOCK_BAR_TWO_LINK)
|
||||
self.linkIcon.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.linkIcon.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.linkChild = SingleLinkItem(self, mainWindow)
|
||||
self.linkChild.setStyleSheet(Stylesheets.DOCK_BAR_TWO_LINK)
|
||||
oneLinkRelLayout.addWidget(self.linkParent)
|
||||
oneLinkRelLayout.addWidget(self.linkIcon)
|
||||
oneLinkRelLayout.addWidget(self.linkChild)
|
||||
@@ -276,7 +275,7 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
return
|
||||
rowCount = 0
|
||||
for key in jsonDict:
|
||||
if key in ["uid", "Child UIDs", "Icon"]:
|
||||
if key in hidden_fields_dockbars:
|
||||
continue
|
||||
elif key == "Notes":
|
||||
notesTextArea = RichNotesEditor(self, jsonDict[key], False)
|
||||
@@ -284,13 +283,16 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
self.detailsLayoutOneNode.addWidget(notesTextArea, rowCount, 1, 10, 1)
|
||||
rowCount += 9
|
||||
else:
|
||||
valueLabel = QtWidgets.QLineEdit(str(jsonDict[key]))
|
||||
valueLabel.setReadOnly(True)
|
||||
|
||||
self.detailsLayoutOneNode.addWidget(QtWidgets.QLabel(key), rowCount, 0)
|
||||
self.detailsLayoutOneNode.addWidget(QtWidgets.QLabel(str(jsonDict[key])), rowCount, 1)
|
||||
self.detailsLayoutOneNode.addWidget(valueLabel, rowCount, 1)
|
||||
rowCount += 1
|
||||
filePath = jsonDict.get('File Path')
|
||||
if filePath is not None:
|
||||
fullFilePath = Path(self.mainWindow.SETTINGS.value('Project/FilesDir')) / filePath
|
||||
if fullFilePath.exists() and fullFilePath.is_file():
|
||||
if fullFilePath.is_file():
|
||||
magicType = magic.from_file(str(fullFilePath), mime=True)
|
||||
if magicType.split('/')[0] == 'image':
|
||||
previewImage = QtGui.QImage(fullFilePath)
|
||||
@@ -304,7 +306,9 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
else:
|
||||
previewPixmap = QtGui.QPixmap(previewImage)
|
||||
previewLabel = QtWidgets.QLabel()
|
||||
previewLabel.setPixmap(previewPixmap.scaled(250, 250, QtCore.Qt.KeepAspectRatio))
|
||||
previewLabel.setPixmap(previewPixmap.scaled(250,
|
||||
250,
|
||||
QtCore.Qt.AspectRatioMode.KeepAspectRatio))
|
||||
self.detailsLayoutOneNode.addWidget(QtWidgets.QLabel('Preview:'), rowCount, 0)
|
||||
self.detailsLayoutOneNode.addWidget(previewLabel, rowCount, 1, 10, 1)
|
||||
|
||||
@@ -314,9 +318,9 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
second = uid[1]
|
||||
secondJson = self.entityDB.getEntity(second)
|
||||
firstPixmap = QtGui.QPixmap()
|
||||
firstPixmap.loadFromData(firstJson.get('Icon'))
|
||||
firstPixmap.loadFromData(resizePictureFromBuffer(firstJson.get('Icon'), (40, 40)))
|
||||
secondPixmap = QtGui.QPixmap()
|
||||
secondPixmap.loadFromData(secondJson.get('Icon'))
|
||||
secondPixmap.loadFromData(resizePictureFromBuffer(secondJson.get('Icon'), (40, 40)))
|
||||
self.linkParent.linkItemPic.setPixmap(firstPixmap)
|
||||
self.linkParent.linkItemName.setText(firstJson[list(firstJson)[1]])
|
||||
self.linkParent.linkItemUid = firstJson['uid']
|
||||
@@ -331,7 +335,7 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
uid = edge[0]
|
||||
edgeJson = self.entityDB.getEntity(uid)
|
||||
nodePixmap = QtGui.QPixmap()
|
||||
nodePixmap.loadFromData(edgeJson.get('Icon'))
|
||||
nodePixmap.loadFromData(resizePictureFromBuffer(edgeJson.get('Icon'), (40, 40)))
|
||||
ResolutionTreeWidgetEntity(self.relationshipsIncomingTable,
|
||||
nodePixmap,
|
||||
edgeJson[list(edgeJson)[1]],
|
||||
@@ -341,7 +345,7 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
uid = edge[1]
|
||||
edgeJson = self.entityDB.getEntity(uid)
|
||||
nodePixmap = QtGui.QPixmap()
|
||||
nodePixmap.loadFromData(edgeJson.get('Icon'))
|
||||
nodePixmap.loadFromData(resizePictureFromBuffer(edgeJson.get('Icon'), (40, 40)))
|
||||
ResolutionTreeWidgetEntity(self.relationshipsOutgoingTable,
|
||||
nodePixmap,
|
||||
edgeJson[list(edgeJson)[1]],
|
||||
@@ -352,7 +356,7 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
inc = len(self.entityDB.getIncomingLinks(nodeJson['uid']))
|
||||
out = len(self.entityDB.getOutgoingLinks(nodeJson['uid']))
|
||||
nodePixmap = QtGui.QPixmap()
|
||||
nodePixmap.loadFromData(nodeJson.get('Icon'))
|
||||
nodePixmap.loadFromData(resizePictureFromBuffer(nodeJson.get('Icon'), (40, 40)))
|
||||
ResolutionTreeWidgetEntity(self.nodesTable,
|
||||
nodePixmap,
|
||||
nodeJson[list(nodeJson)[1]],
|
||||
@@ -377,7 +381,7 @@ class EntityDetails(QtWidgets.QWidget):
|
||||
self.entityPrimaryLabel.setText(jsonDict[list(jsonDict)[0]])
|
||||
self.entityTypeLabel.setText(jsonDict['Entity Type'])
|
||||
summaryPixmap = QtGui.QPixmap()
|
||||
summaryPixmap.loadFromData(jsonDict.get('Icon'))
|
||||
summaryPixmap.loadFromData(resizePictureFromBuffer(jsonDict.get('Icon'), (40, 40)))
|
||||
self.summaryIcon.setPixmap(summaryPixmap)
|
||||
else:
|
||||
self.entityUIDLabel.setText("--")
|
||||
@@ -415,10 +419,11 @@ class SingleLinkItem(QtWidgets.QWidget):
|
||||
|
||||
self.linkItemPic = QtWidgets.QLabel()
|
||||
|
||||
self.linkItemPic.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.linkItemName = QtWidgets.QLabel()
|
||||
self.linkItemPic.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.linkItemName = QtWidgets.QLineEdit()
|
||||
self.linkItemName.setReadOnly(True)
|
||||
|
||||
self.linkItemName.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.linkItemName.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.linkItemUid = ""
|
||||
self.setMaximumHeight(90)
|
||||
|
||||
@@ -441,7 +446,6 @@ class RelationshipsTable(QtWidgets.QTreeWidget):
|
||||
def __init__(self, parent, mainWindow, uidLabel: QtWidgets.QLabel = None, incomingOrOutgoing: int = None):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.mainWindow = mainWindow
|
||||
self.incomingOrOutgoing = incomingOrOutgoing
|
||||
self.uidLabel = uidLabel
|
||||
@@ -474,7 +478,6 @@ class LinksTable(QtWidgets.QTreeWidget):
|
||||
def __init__(self, parent, mainWindow):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.mainWindow = mainWindow
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
@@ -542,9 +545,7 @@ class Oracle(QtWidgets.QWidget):
|
||||
self.setLayout(oracleLayout)
|
||||
|
||||
self.answerLabel = QtWidgets.QLabel("Answer Section")
|
||||
self.answerLabel.setStyleSheet(Stylesheets.DOCK_BAR_LABEL)
|
||||
self.answerLabel.setAlignment(QtCore.Qt.AlignHCenter |
|
||||
QtCore.Qt.AlignVCenter)
|
||||
self.answerLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
oracleLayout.addWidget(self.answerLabel, 1, 0, 1, 2)
|
||||
self.answerSection = QtWidgets.QPlainTextEdit()
|
||||
self.answerSection.setReadOnly(True)
|
||||
@@ -554,9 +555,7 @@ class Oracle(QtWidgets.QWidget):
|
||||
oracleLayout.addWidget(self.answerSection, 2, 0, 1, 2)
|
||||
|
||||
self.questionLabel = QtWidgets.QLabel("Ask a Question")
|
||||
self.questionLabel.setStyleSheet(Stylesheets.DOCK_BAR_LABEL)
|
||||
self.questionLabel.setAlignment(QtCore.Qt.AlignHCenter |
|
||||
QtCore.Qt.AlignVCenter)
|
||||
self.questionLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
oracleLayout.addWidget(self.questionLabel, 3, 0, 1, 2)
|
||||
self.questionSection = QtWidgets.QLineEdit()
|
||||
self.questionSection.setPlaceholderText("Ask a Question here.")
|
||||
|
||||
@@ -10,6 +10,8 @@ from PySide6.QtWidgets import QGraphicsItem
|
||||
from PySide6.QtWidgets import QGraphicsItemGroup, QGraphicsSimpleTextItem, QGraphicsPixmapItem, QGraphicsTextItem
|
||||
from PySide6.QtSvgWidgets import QGraphicsSvgItem
|
||||
|
||||
from Core.ResourceHandler import resizePictureFromBuffer
|
||||
|
||||
ENTITY_TEXT_FONT = QtGui.QFont("Mono", 11, 700)
|
||||
LINK_TEXT_FONT = QtGui.QFont("Mono", 11, 700)
|
||||
|
||||
@@ -20,32 +22,36 @@ class BaseNode(QGraphicsItemGroup):
|
||||
brush: QtGui.QBrush) -> None:
|
||||
super(BaseNode, self).__init__()
|
||||
|
||||
self.setCacheMode(self.DeviceCoordinateCache)
|
||||
self.setCacheMode(QGraphicsItemGroup.CacheMode.DeviceCoordinateCache)
|
||||
resizedByteArray = resizePictureFromBuffer(pictureByteArray, (40, 40))
|
||||
|
||||
self.pixmapItem = QtGui.QPixmap()
|
||||
self.pixmapItem.loadFromData(pictureByteArray)
|
||||
|
||||
if pictureByteArray.data().startswith(b'<svg '):
|
||||
if pictureByteArray.data().startswith(b'<svg ') or pictureByteArray.data().startswith(b'<?xml'):
|
||||
self.iconItem = QGraphicsSvgItem()
|
||||
self.iconItem.renderer().load(pictureByteArray)
|
||||
self.iconItem.renderer().load(resizedByteArray)
|
||||
# Force recalculation of geometry, else this looks like 1 pixel.
|
||||
# https://stackoverflow.com/a/68182093
|
||||
self.iconItem.setElementId("")
|
||||
else:
|
||||
self.iconItem = QGraphicsPixmapItem(self.pixmapItem)
|
||||
pixmapItem = QtGui.QPixmap()
|
||||
pixmapItem.loadFromData(resizedByteArray)
|
||||
self.iconItem = QGraphicsPixmapItem(pixmapItem)
|
||||
|
||||
self.labelItem = QGraphicsTextItem('')
|
||||
# Have to do it this way; directly assigning stuff does not work due to how PySide6 works.
|
||||
labelDocument = self.labelItem.document()
|
||||
labelDocument.setTextWidth(280)
|
||||
textOption = labelDocument.defaultTextOption()
|
||||
textOption.setWrapMode(QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere)
|
||||
textOption.setAlignment(QtCore.Qt.AlignHCenter)
|
||||
textOption.setWrapMode(QtGui.QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere)
|
||||
textOption.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter)
|
||||
labelDocument.setDefaultTextOption(textOption)
|
||||
self.labelItem.setDocument(labelDocument)
|
||||
|
||||
self.bannerIconItem = QGraphicsSvgItem()
|
||||
self.bannerIconItem.setElementId("")
|
||||
|
||||
self.addToGroup(self.iconItem)
|
||||
self.addToGroup(self.labelItem)
|
||||
self.addToGroup(self.bannerIconItem)
|
||||
|
||||
if font is not None:
|
||||
self.labelItem.setFont(font)
|
||||
else:
|
||||
@@ -53,9 +59,11 @@ class BaseNode(QGraphicsItemGroup):
|
||||
if brush is not None:
|
||||
self.labelItem.setDefaultTextColor(brush.color())
|
||||
|
||||
self.labelItem.setPos(self.iconItem.x() - 120, self.iconItem.y() + 45)
|
||||
self.updateLabel(primaryAttribute)
|
||||
|
||||
self.bannerIconItem.setPos(self.iconItem.x() + 15, self.iconItem.y() - 9)
|
||||
self.bannerIconItem.setZValue(10)
|
||||
|
||||
self.uid = uid
|
||||
self.setFlag(QGraphicsItem.ItemIsMovable, True)
|
||||
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
|
||||
@@ -64,17 +72,24 @@ class BaseNode(QGraphicsItemGroup):
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
self.connectors = []
|
||||
self.bookmarked = False
|
||||
self.isBeingResolved = False
|
||||
self.parentGroup = None
|
||||
|
||||
def updateLabel(self, newText: str = '') -> None:
|
||||
if not isinstance(newText, str):
|
||||
newText = newText
|
||||
if newText != '':
|
||||
if len(newText) > 50:
|
||||
newText = f"{newText[:47]}..."
|
||||
self.labelItem.setPlainText(newText)
|
||||
self.labelItem.document().adjustSize()
|
||||
self.labelItem.setPos(self.iconItem.x() + 20 - (self.labelItem.textWidth() / 2), self.iconItem.y() + 45)
|
||||
|
||||
def updateBanner(self, bannerHidden: bool = True, bannerGraphic: QtCore.QByteArray = None) -> None:
|
||||
if bannerHidden: # No icon visible
|
||||
self.bannerIconItem.hide()
|
||||
self.bannerIconItem.setVisible(False)
|
||||
return
|
||||
self.bannerIconItem.renderer().load(bannerGraphic)
|
||||
self.bannerIconItem.show()
|
||||
self.bannerIconItem.setVisible(True)
|
||||
self.bannerIconItem.setElementId("")
|
||||
|
||||
def removeConnector(self, connector) -> None:
|
||||
# Exception could be thrown if the connector is already deleted.
|
||||
@@ -117,9 +132,14 @@ class BaseNode(QGraphicsItemGroup):
|
||||
|
||||
def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionGraphicsItem,
|
||||
widget: Optional[QtWidgets.QWidget] = ...) -> None:
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
painter.setPen(QtCore.Qt.PenStyle.NoPen)
|
||||
if self.scene().views()[0].zoom < self.scene().hideZoom:
|
||||
self.labelItem.hide()
|
||||
# Looks stupid, but fixes bug where entities are deselected when zooming out past hideZoom level.
|
||||
if self.isSelected():
|
||||
self.labelItem.hide()
|
||||
self.setSelected(True)
|
||||
else:
|
||||
self.labelItem.hide()
|
||||
else:
|
||||
self.labelItem.show()
|
||||
if self.isSelected():
|
||||
@@ -145,7 +165,7 @@ class GroupNode(BaseNode):
|
||||
self.listProxyWidget = None
|
||||
|
||||
def itemChange(self, change: QtWidgets.QGraphicsItem.GraphicsItemChange, value: Any) -> Any:
|
||||
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
|
||||
if change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemSelectedChange:
|
||||
if value:
|
||||
self.showList(None)
|
||||
else:
|
||||
@@ -163,7 +183,7 @@ class GroupNode(BaseNode):
|
||||
except IndexError:
|
||||
primaryField = ''
|
||||
iconPixmap = QtGui.QPixmap()
|
||||
iconPixmap.loadFromData(entityJson['Icon'])
|
||||
iconPixmap.loadFromData(resizePictureFromBuffer(entityJson['Icon'], (40, 40)))
|
||||
|
||||
GroupNodeListItem(icon=iconPixmap, text=primaryField, uid=uid,
|
||||
listview=self.listWidget.itemList)
|
||||
@@ -185,7 +205,8 @@ class GroupNode(BaseNode):
|
||||
def formGroup(self, childNodeUIDs, listProxyWidget: QtWidgets.QGraphicsProxyWidget) -> None:
|
||||
[self.addItemToGroup(uid) for uid in childNodeUIDs] # Should be faster than just a for loop
|
||||
self.listProxyWidget = listProxyWidget
|
||||
self.listProxyWidget.setCacheMode(self.DeviceCoordinateCache)
|
||||
self.listProxyWidget.setCacheMode(QGraphicsItemGroup.CacheMode.DeviceCoordinateCache)
|
||||
self.listProxyWidget.setZValue(100)
|
||||
|
||||
def addItemToGroup(self, uid: str) -> None:
|
||||
self.groupedNodesUid.add(uid)
|
||||
@@ -250,8 +271,8 @@ class BaseConnector(QGraphicsItemGroup):
|
||||
self.colorDefault = QtGui.QColor(200, 200, 200)
|
||||
self.myColor = self.colorDefault
|
||||
|
||||
self.pen = QtGui.QPen(self.myColor, 2, QtCore.Qt.SolidLine,
|
||||
QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
self.pen = QtGui.QPen(self.myColor, 2, QtCore.Qt.PenStyle.SolidLine,
|
||||
QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin)
|
||||
|
||||
self.arrowHead = QtGui.QPolygonF()
|
||||
self.line = QtCore.QLineF()
|
||||
@@ -281,10 +302,6 @@ class BaseConnector(QGraphicsItemGroup):
|
||||
def endItem(self) -> BaseNode:
|
||||
return self.myEndItem
|
||||
|
||||
def updatePosition(self) -> None:
|
||||
self.line = QtCore.QLineF(self.mapFromItem(self.myStartItem, 0, 0), self.mapFromItem(self.myEndItem, 0, 0))
|
||||
self.update()
|
||||
|
||||
def boundingRect(self) -> QtCore.QRectF:
|
||||
extra = self.pen.width() + 20
|
||||
p1 = self.line.p1()
|
||||
@@ -301,8 +318,8 @@ class BaseConnector(QGraphicsItemGroup):
|
||||
def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionGraphicsItem,
|
||||
widget: Optional[QtWidgets.QWidget] = ...) -> None:
|
||||
|
||||
currentStartPos = self.myStartItem.pos()
|
||||
currentEndPos = self.myEndItem.pos()
|
||||
currentStartPos = self.myStartItem.pos() - self.pos()
|
||||
currentEndPos = self.myEndItem.pos() - self.pos()
|
||||
|
||||
self.myColor = self.colorSelected if self.isSelected() else self.colorDefault
|
||||
|
||||
@@ -312,14 +329,22 @@ class BaseConnector(QGraphicsItemGroup):
|
||||
line = QtCore.QLineF(p1, p2)
|
||||
|
||||
if line.length() < 45:
|
||||
self.labelItem.hide()
|
||||
if self.isSelected():
|
||||
self.labelItem.hide()
|
||||
self.setSelected(True)
|
||||
else:
|
||||
self.labelItem.hide()
|
||||
return
|
||||
|
||||
angle = math.atan2(line.dy(), - line.dx())
|
||||
|
||||
if (line.length() < 50 + len(self.labelItem.text()) * 15) or \
|
||||
self.scene().views()[0].zoom < self.scene().hideZoom:
|
||||
self.labelItem.hide()
|
||||
if self.isSelected():
|
||||
self.labelItem.hide()
|
||||
self.setSelected(True)
|
||||
else:
|
||||
self.labelItem.hide()
|
||||
else:
|
||||
self.labelItem.show()
|
||||
angle2 = math.degrees(math.pi - angle)
|
||||
@@ -371,7 +396,7 @@ class GroupNodeChildList(QtWidgets.QWidget):
|
||||
|
||||
self.setLayout(QtWidgets.QVBoxLayout())
|
||||
titleLabel = QtWidgets.QLabel('Child Items')
|
||||
titleLabel.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter)
|
||||
titleLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.layout().addWidget(titleLabel)
|
||||
|
||||
self.itemList = ChildListWidget()
|
||||
@@ -386,7 +411,11 @@ class ChildListWidget(QtWidgets.QListWidget):
|
||||
self.setSortingEnabled(True)
|
||||
|
||||
def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
|
||||
itemDragged = self.itemAt(event.pos())
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
itemDragged = None
|
||||
if event.buttons() == QtCore.Qt.MouseButton.LeftButton:
|
||||
itemDragged = self.itemAt(event.pos())
|
||||
|
||||
if itemDragged is None:
|
||||
return
|
||||
@@ -405,5 +434,3 @@ class ChildListWidget(QtWidgets.QListWidget):
|
||||
drag.setPixmap(pixmap)
|
||||
drag.setHotSpot(QtCore.QPoint(pixmap.rect().width() / 2, pixmap.rect().height() / 2))
|
||||
drag.exec_()
|
||||
|
||||
super().mousePressEvent(event)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,273 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
TOOLBAR_STYLESHEET = """QToolBar {background-color: rgb(41, 45, 62);
|
||||
color: rgba(248, 248, 242, 1) !important;
|
||||
font-family: Segoe UI;
|
||||
font-size: 13px;
|
||||
text-align: left;}
|
||||
|
||||
QToolBar::separator {
|
||||
background-color: rgb(0, 173, 238);
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
}
|
||||
"""
|
||||
|
||||
MAIN_WINDOW_STYLESHEET = """
|
||||
QWidget{
|
||||
background-color: rgb(41, 45, 62);
|
||||
color: rgba(248, 248, 242, 1) !important;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
QScrollBar:vertical {
|
||||
background:rgb(44, 49, 58);
|
||||
width:7px;
|
||||
margin: 0px 0px 0px 0px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:vertical {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||||
stop: 0 rgb(103, 110, 149), stop: 0.5 rgb(103, 110, 149), stop:1 rgb(103, 110, 149));
|
||||
min-height: 0px;
|
||||
}
|
||||
|
||||
QScrollBar:horizontal {
|
||||
background:rgb(44, 49, 58);
|
||||
height:7px;
|
||||
margin: 0px 0px 0px 0px;
|
||||
}
|
||||
|
||||
QScrollBar::handle:horizontal {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||||
stop: 0 rgb(103, 110, 149),
|
||||
stop: 0.5 rgb(103, 110, 149),
|
||||
stop:1 rgb(103, 110, 149));
|
||||
}
|
||||
QMenuBar {
|
||||
color: #ffffff;
|
||||
background-color: rgb(33, 37, 43);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
QPushButton {
|
||||
border: 2px solid rgb(0, 173, 238);
|
||||
border-radius: 6px;
|
||||
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
||||
stop: 1 rgb(0, 173, 238), stop: 0.7 rgb(44, 49, 58));
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
QLabel {
|
||||
border: 2px solid rgb(41, 45, 62);
|
||||
padding-left: 7px;
|
||||
border-left-color: rgb(0, 173, 238);
|
||||
}
|
||||
|
||||
QLineEdit, QPlainTextEdit {
|
||||
border: 0.5px solid rgb(0, 173, 238);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
QTabBar::tab {
|
||||
background: rgb(68, 66, 103);
|
||||
border: 2px solid rgb(41, 45, 62);
|
||||
border-radius: 3px;
|
||||
min-height: 3ex;
|
||||
}
|
||||
|
||||
QTabWidget {
|
||||
border-style: outset;
|
||||
border-color: rgba(248, 248, 242, 1);
|
||||
border-width: 0.5px;
|
||||
}
|
||||
|
||||
QToolBar {
|
||||
border-style: outset;
|
||||
border-color: rgba(75, 75, 75, 1);
|
||||
border-width: 1px;
|
||||
border-left-width: 0px;
|
||||
border-right-width: 0px;
|
||||
}
|
||||
|
||||
QTabBar::tab:selected {
|
||||
background: rgb(51, 55, 95);
|
||||
border: 2px solid rgb(41, 45, 62);
|
||||
min-height: 2.5ex;
|
||||
border-radius: 3px;
|
||||
border-top-color: rgb(0, 173, 238);
|
||||
}
|
||||
|
||||
QComboBox { combobox-popup: 0; }
|
||||
|
||||
QHeaderView::section {
|
||||
background-color: rgb(41, 45, 62);
|
||||
color: rgba(248, 248, 242, 1);
|
||||
}
|
||||
|
||||
QMenu::item {
|
||||
background-color: rgb(41, 45, 62);
|
||||
color: rgba(248, 248, 242, 1) !important;
|
||||
border-left: 1px solid rgb(0, 173, 238);
|
||||
padding-right: 4px;
|
||||
padding-bottom: 4px;
|
||||
padding-top: 4px;
|
||||
font-size: 15px;
|
||||
text-align: left;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
QMenu::item:selected {
|
||||
background-color: rgb(0, 85, 127);
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
QMenu::item:disabled {
|
||||
background-color:rgb(81, 87, 114);
|
||||
}
|
||||
|
||||
QTextBrowser {
|
||||
background-color:rgb(60, 60, 80);
|
||||
}
|
||||
"""
|
||||
|
||||
DOCK_BAR_TWO_LINK = """
|
||||
QLabel {
|
||||
border: 1px solid rgb(41, 45, 62);
|
||||
}
|
||||
"""
|
||||
|
||||
DOCK_BAR_LABEL = """
|
||||
QLabel {
|
||||
border: 1px solid rgb(41, 45, 62);
|
||||
border-radius: 2px;
|
||||
border-bottom-color: rgb(0, 173, 238);
|
||||
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
||||
stop: 1 rgb(0, 173, 238), stop: 0.85 rgb(41, 45, 62));
|
||||
}
|
||||
"""
|
||||
|
||||
TEXT_BOX_STYLESHEET = "QLineEdit, QLabel{" \
|
||||
"border-bottom: 1px solid rgb(0, 173, 238);" \
|
||||
"}"
|
||||
|
||||
CHECK_BOX_STYLESHEET = "QCheckBox::indicator:unchecked" \
|
||||
"{" \
|
||||
"border: 0.5px solid rgb(0, 173, 238);" \
|
||||
"background: none;" \
|
||||
"}"
|
||||
|
||||
RADIO_BUTTON_STYLESHEET = "QRadioButton::indicator:unchecked" \
|
||||
"{" \
|
||||
"border: 0.5px solid rgb(0, 173, 238);" \
|
||||
"background: none;" \
|
||||
"border-radius: 7px;" \
|
||||
"}"
|
||||
|
||||
SETTINGS_WIDGET_STYLESHEET = "#settingsWidget {background-color:rgb(41, 45, 62);}"
|
||||
|
||||
MENUS_STYLESHEET = "background-color: rgb(41, 45, 62);" \
|
||||
"color: rgba(248, 248, 242, 1) !important;" \
|
||||
"border: 1px solid rgb(44, 49, 58);" \
|
||||
"border-bottom: 1px solid rgb(0, 173, 238);" \
|
||||
"font-family: Segoe UI;" \
|
||||
"font-size: 13px;" \
|
||||
"text-align: left;"
|
||||
|
||||
MENUS_STYLESHEET_2 = """QMenu::item{
|
||||
background-color: rgb(41, 45, 62);
|
||||
color: rgba(248, 248, 242, 1) !important;
|
||||
border-left: 1px solid rgb(0, 173, 238);
|
||||
padding-right: 4px;
|
||||
padding-bottom: 4px;
|
||||
padding-top: 4px;
|
||||
font-size: 15px;
|
||||
text-align: left;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
QMenu::item:selected{
|
||||
background-color: rgb(0, 85, 127);
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
QMenu::item:disabled {
|
||||
background-color:rgb(81, 87, 114);
|
||||
}"""
|
||||
|
||||
MERGE_STYLESHEET = """
|
||||
QWidget, QDialog{
|
||||
background-color: rgb(41, 45, 62);
|
||||
color: rgba(248, 248, 242, 1) !important;
|
||||
font-family: Segoe UI;
|
||||
font-size: 13px;}
|
||||
|
||||
QScrollBar:vertical {
|
||||
background:rgb(44, 49, 58);
|
||||
width:7px;
|
||||
margin: 0px 0px 0px 0px;
|
||||
}
|
||||
QScrollBar::handle:vertical {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||||
stop: 0 rgb(103, 110, 149), stop: 0.5 rgb(103, 110, 149), stop:1 rgb(103, 110, 149));
|
||||
min-height: 0px;
|
||||
}
|
||||
QScrollBar:horizontal {
|
||||
background:rgb(44, 49, 58);
|
||||
height:7px;
|
||||
margin: 0px 0px 0px 0px;
|
||||
}
|
||||
QScrollBar::handle:horizontal {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||||
stop: 0 rgb(103, 110, 149), stop: 0.5 rgb(103, 110, 149), stop:1 rgb(103, 110, 149));
|
||||
}"""
|
||||
|
||||
RESOLUTION_WIZARD_STYLESHEET = "background-color: rgb(41, 45, 62);" \
|
||||
"color: rgba(248, 248, 242, 1) !important;" \
|
||||
"padding-bottom: 5px;" \
|
||||
"font-family: Segoe UI;" \
|
||||
"font-size: 13px;"
|
||||
|
||||
PATH_INPUT_STYLESHEET = """border: 2px solid rgb(44, 49, 58);
|
||||
border-radius: 25px;
|
||||
padding: 4px;
|
||||
background-color: rgb(129, 133, 137);
|
||||
color: rgba(248, 248, 242, 1) !important;
|
||||
"""
|
||||
|
||||
BUTTON_STYLESHEET = """
|
||||
QPushButton {
|
||||
border: 2px solid rgb(0, 173, 238);
|
||||
border-radius: 6px;
|
||||
color: rgba(248, 248, 242, 1) !important;
|
||||
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
||||
stop: 1 rgb(0, 173, 238), stop: 0.7 rgb(44, 49, 58));
|
||||
min-width: 80px;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
||||
stop: 0 #dadbde, stop: 1 #f6f7fa);
|
||||
}
|
||||
"""
|
||||
|
||||
BUTTON_STYLESHEET_2 = """
|
||||
QPushButton {
|
||||
border: 2px solid rgb(0, 173, 238);
|
||||
border-radius: 6px;
|
||||
color: rgba(248, 248, 242, 1) !important;
|
||||
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
||||
stop: 1 rgb(0, 173, 238), stop: 0.7 rgb(44, 49, 58));
|
||||
|
||||
min-width: 300px;
|
||||
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
|
||||
stop: 0 #dadbde, stop: 1 #f6f7fa);
|
||||
}
|
||||
"""
|
||||
|
||||
SELECT_PROJECT_STYLESHEET = "background-color: rgb(41, 45, 62);" \
|
||||
"color: rgba(248, 248, 242, 1) !important;" \
|
||||
"border-color: rgb(0, 173, 238);"
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from PySide6 import QtWidgets, QtGui
|
||||
from Core.Interface import Stylesheets
|
||||
|
||||
|
||||
class ToolBarOne(QtWidgets.QToolBar):
|
||||
@@ -10,8 +9,7 @@ class ToolBarOne(QtWidgets.QToolBar):
|
||||
# Parent is (expected to be) mainWindow.
|
||||
super().__init__(title, parent=parent)
|
||||
self.setObjectName(title)
|
||||
self.setToolButtonStyle(QtGui.Qt.ToolButtonTextUnderIcon)
|
||||
self.setStyleSheet(Stylesheets.TOOLBAR_STYLESHEET)
|
||||
self.setToolButtonStyle(QtGui.Qt.ToolButtonStyle.ToolButtonTextUnderIcon)
|
||||
|
||||
newCanvas = QtGui.QAction("Add Canvas",
|
||||
self,
|
||||
|
||||
905
Core/LQL.py
905
Core/LQL.py
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,6 @@ from logging import handlers
|
||||
from multiprocessing import Queue
|
||||
from pathlib import Path
|
||||
from PySide6 import QtWidgets
|
||||
from Core.Interface import Stylesheets
|
||||
|
||||
|
||||
class MessageHandler:
|
||||
@@ -26,7 +25,6 @@ class MessageHandler:
|
||||
self.linkScopeLogger.info(message, exc_info=exc_info)
|
||||
if popUp:
|
||||
msgBox = QtWidgets.QMessageBox()
|
||||
msgBox.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
QtWidgets.QMessageBox.information(msgBox,
|
||||
self.mainWindow.tr("Info"),
|
||||
self.mainWindow.tr(message))
|
||||
@@ -36,7 +34,6 @@ class MessageHandler:
|
||||
self.linkScopeLogger.warning(message, exc_info=exc_info)
|
||||
if popUp:
|
||||
msgBox = QtWidgets.QMessageBox()
|
||||
msgBox.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
QtWidgets.QMessageBox.warning(msgBox,
|
||||
self.mainWindow.tr("Warning"),
|
||||
self.mainWindow.tr(message))
|
||||
@@ -46,7 +43,6 @@ class MessageHandler:
|
||||
self.linkScopeLogger.error(message, exc_info=exc_info)
|
||||
if popUp:
|
||||
msgBox = QtWidgets.QMessageBox()
|
||||
msgBox.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
QtWidgets.QMessageBox.critical(msgBox,
|
||||
self.mainWindow.tr("Error"),
|
||||
self.mainWindow.tr(message))
|
||||
@@ -56,7 +52,6 @@ class MessageHandler:
|
||||
self.linkScopeLogger.critical(message, exc_info=exc_info)
|
||||
if popUp:
|
||||
msgBox = QtWidgets.QMessageBox()
|
||||
msgBox.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
QtWidgets.QMessageBox.critical(msgBox,
|
||||
self.mainWindow.tr("Critical"),
|
||||
self.mainWindow.tr(message))
|
||||
|
||||
1000
Core/ModuleManager.py
Normal file
1000
Core/ModuleManager.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import tempfile
|
||||
import re
|
||||
from shutil import rmtree
|
||||
from svglib.svglib import svg2rlg
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from reportlab.lib import colors
|
||||
@@ -15,10 +20,369 @@ from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
|
||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||
from reportlab.lib.pagesizes import LETTER, inch
|
||||
from reportlab.graphics.shapes import Line, Drawing
|
||||
from PySide6 import QtWidgets
|
||||
|
||||
from Core.Interface.Entity import BaseNode
|
||||
from Core.GlobalVariables import avoid_parsing_fields
|
||||
|
||||
|
||||
class ReportWizard(QtWidgets.QWizard):
|
||||
def __init__(self, parent):
|
||||
super(ReportWizard, self).__init__(parent=parent)
|
||||
self.reportTempFolder = tempfile.mkdtemp()
|
||||
|
||||
self.primaryFieldsList = []
|
||||
|
||||
self.addPage(InitialConfigPage(self))
|
||||
self.addPage(TitlePage(self))
|
||||
|
||||
self.addPage(SummaryPage(self))
|
||||
|
||||
self.selectedNodes = [entity for entity in
|
||||
self.parent().centralWidget().tabbedPane.getCurrentScene().selectedItems()
|
||||
if isinstance(entity, BaseNode)]
|
||||
for selectedNode in self.selectedNodes:
|
||||
# used in wizard
|
||||
self.primaryField = selectedNode.labelItem.toPlainText()
|
||||
self.uid = selectedNode.uid
|
||||
|
||||
# used in report generation
|
||||
self.primaryFieldsList.append(self.primaryField)
|
||||
self.addPage(EntityPage(self))
|
||||
|
||||
self.setWizardStyle(QtWidgets.QWizard.WizardStyle.ModernStyle)
|
||||
self.setWindowTitle("Generate Report Wizard")
|
||||
|
||||
self.button(QtWidgets.QWizard.WizardButton.FinishButton).clicked.connect(self.onFinish)
|
||||
|
||||
def onFinish(self):
|
||||
outgoingEntitiesForEachEntity = []
|
||||
incomingEntitiesForEachEntity = []
|
||||
outgoingEntityPrimaryFieldsForEachEntity = []
|
||||
incomingEntityPrimaryFieldsForEachEntity = []
|
||||
|
||||
entityList = []
|
||||
reportData = []
|
||||
for pageID in self.pageIds():
|
||||
pageObject = self.page(pageID)
|
||||
reportData.append(pageObject.getData())
|
||||
|
||||
for selectedNode in self.selectedNodes:
|
||||
uid = selectedNode.uid
|
||||
entityList.append(self.parent().LENTDB.getEntity(uid))
|
||||
outgoing = self.parent().LENTDB.getOutgoingLinks(uid)
|
||||
incoming = self.parent().LENTDB.getIncomingLinks(uid)
|
||||
|
||||
outgoingEntities = []
|
||||
incomingEntities = []
|
||||
outgoingNames = []
|
||||
incomingNames = []
|
||||
|
||||
for out in outgoing:
|
||||
outLink = self.parent().LENTDB.getLink(out)
|
||||
outgoingEntities.append(outLink)
|
||||
outgoingEntityJson = self.parent().LENTDB.getEntity(outLink['uid'][1])
|
||||
outgoingNames.append(outgoingEntityJson[list(outgoingEntityJson)[1]])
|
||||
|
||||
for inc in incoming:
|
||||
inLink = self.parent().LENTDB.getLink(inc)
|
||||
incomingEntities.append(inLink)
|
||||
incomingEntityJson = self.parent().LENTDB.getEntity(inLink['uid'][0])
|
||||
|
||||
incomingNames.append(incomingEntityJson[list(incomingEntityJson)[1]])
|
||||
|
||||
outgoingEntityPrimaryFieldsForEachEntity.append(outgoingNames)
|
||||
incomingEntityPrimaryFieldsForEachEntity.append(incomingNames)
|
||||
outgoingEntitiesForEachEntity.append(outgoingEntities)
|
||||
incomingEntitiesForEachEntity.append(incomingEntities)
|
||||
|
||||
path = Path(reportData[0].get('SavePath'))
|
||||
|
||||
canvasName = reportData[2].get('CanvasName')
|
||||
viewPortBool = reportData[2].get('ViewPort')
|
||||
|
||||
canvasPicture = self.parent().getPictureOfCanvas(canvasName, viewPortBool, True)
|
||||
canvasImagePath = Path(self.reportTempFolder) / 'canvas.png'
|
||||
canvasPicture.save(str(canvasImagePath), "PNG")
|
||||
|
||||
# timelinePicture = self.parent().dockbarThree.timeWidget.takePictureOfView(False)
|
||||
# timelineImagePath = Path(temp_dir.name) / 'timeline.png'
|
||||
# timelinePicture.save(str(timelineImagePath), "PNG")
|
||||
|
||||
savePath = Path(reportData[0]['SavePath']).absolute()
|
||||
|
||||
try:
|
||||
PDFReport(str(path), reportData, outgoingEntitiesForEachEntity,
|
||||
incomingEntitiesForEachEntity, entityList, canvasImagePath, None, # <timelinePic
|
||||
self.primaryFieldsList, incomingEntityPrimaryFieldsForEachEntity,
|
||||
outgoingEntityPrimaryFieldsForEachEntity)
|
||||
|
||||
self.parent().MESSAGEHANDLER.debug(reportData)
|
||||
self.parent().MESSAGEHANDLER.info(
|
||||
f"Saved Report at: {str(savePath)}", popUp=True
|
||||
)
|
||||
except PermissionError:
|
||||
self.parent().MESSAGEHANDLER.error(
|
||||
f"Could not generate report. No permission to save at the chosen location: {str(savePath)}",
|
||||
popUp=True,
|
||||
exc_info=False,
|
||||
)
|
||||
except Exception as exc:
|
||||
self.parent().MESSAGEHANDLER.error(
|
||||
f"Could not generate report: {str(exc)}", popUp=True, exc_info=True
|
||||
)
|
||||
finally:
|
||||
rmtree(self.reportTempFolder)
|
||||
|
||||
|
||||
class InitialConfigPage(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
super(InitialConfigPage, self).__init__(parent)
|
||||
self.subtitleLabel = QtWidgets.QLabel("Path to save the report at: ")
|
||||
self.savePathEdit = QtWidgets.QLineEdit()
|
||||
self.setTitle("Initial Configuration Wizard")
|
||||
|
||||
pDirButton = QtWidgets.QPushButton("Save Report As...")
|
||||
pDirButton.clicked.connect(self.editPath)
|
||||
|
||||
hLayout = QtWidgets.QVBoxLayout()
|
||||
hLayout.addWidget(self.subtitleLabel)
|
||||
hLayout.addWidget(self.savePathEdit)
|
||||
hLayout.addWidget(pDirButton)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addLayout(hLayout)
|
||||
self.setLayout(layout)
|
||||
|
||||
def editPath(self):
|
||||
selectedPath = QtWidgets.QFileDialog.getSaveFileName(self,
|
||||
"File Name to Save As",
|
||||
str(Path.home()),
|
||||
filter="PDF Files (*.pdf)",
|
||||
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog)
|
||||
selectedPath = selectedPath[0]
|
||||
if selectedPath != '':
|
||||
savePath = Path(selectedPath).absolute()
|
||||
if savePath.suffix != '.pdf':
|
||||
savePath = savePath.with_suffix(f"{savePath.suffix}.pdf")
|
||||
self.savePathEdit.setText(str(savePath))
|
||||
|
||||
def getData(self):
|
||||
return {'SavePath': self.savePathEdit.text()}
|
||||
|
||||
|
||||
class TitlePage(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
super(TitlePage, self).__init__(parent)
|
||||
self.inputTitleEdit = QtWidgets.QLineEdit()
|
||||
self.inputSubtitleEdit = QtWidgets.QLineEdit()
|
||||
self.inputAuthorsEdit = QtWidgets.QLineEdit()
|
||||
self.setTitle("Title Page Wizard")
|
||||
|
||||
titleLabel = QtWidgets.QLabel("Title: ")
|
||||
subtitleLabel = QtWidgets.QLabel("Subtitle: ")
|
||||
authorsLabel = QtWidgets.QLabel("Authors: ")
|
||||
|
||||
hLayout = QtWidgets.QVBoxLayout()
|
||||
hLayout.addWidget(titleLabel)
|
||||
hLayout.addWidget(self.inputTitleEdit)
|
||||
hLayout.addWidget(subtitleLabel)
|
||||
hLayout.addWidget(self.inputSubtitleEdit)
|
||||
hLayout.addWidget(authorsLabel)
|
||||
hLayout.addWidget(self.inputAuthorsEdit)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addLayout(hLayout)
|
||||
self.setLayout(layout)
|
||||
|
||||
def getData(self):
|
||||
return {
|
||||
'Title': self.inputTitleEdit.text(),
|
||||
'Subtitle': self.inputSubtitleEdit.text(),
|
||||
'Authors': self.inputAuthorsEdit.text(),
|
||||
}
|
||||
|
||||
|
||||
class SummaryPage(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent):
|
||||
super(SummaryPage, self).__init__(parent=parent.parent())
|
||||
self.setTitle("Summary Page Wizard")
|
||||
self.inputNotesEdit = QtWidgets.QPlainTextEdit()
|
||||
self.canvasDropDownMenu = QtWidgets.QComboBox()
|
||||
self.viewPortCheckBox = QtWidgets.QCheckBox('ViewPort Only')
|
||||
self.viewPortCheckBox.setChecked(False)
|
||||
self.canvasNames = list(self.parent().centralWidget().tabbedPane.canvasTabs.keys())
|
||||
|
||||
summaryLabel = QtWidgets.QLabel("Summary Notes: ")
|
||||
|
||||
canvasLabel = QtWidgets.QLabel("Select canvas to be displayed: ")
|
||||
for canvasName in self.canvasNames:
|
||||
self.canvasDropDownMenu.addItem(canvasName)
|
||||
|
||||
hLayout = QtWidgets.QVBoxLayout()
|
||||
hLayout.addWidget(summaryLabel)
|
||||
hLayout.addWidget(self.inputNotesEdit)
|
||||
hLayout.addWidget(canvasLabel)
|
||||
hLayout.addWidget(self.viewPortCheckBox)
|
||||
hLayout.addWidget(self.canvasDropDownMenu)
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addLayout(hLayout)
|
||||
self.setLayout(layout)
|
||||
|
||||
def getData(self):
|
||||
return {
|
||||
'SummaryNotes': self.inputNotesEdit.toPlainText(),
|
||||
'CanvasName': self.canvasDropDownMenu.currentText(),
|
||||
'ViewPort': self.viewPortCheckBox.isChecked(),
|
||||
}
|
||||
|
||||
|
||||
class EntityPage(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent: ReportWizard):
|
||||
super(EntityPage, self).__init__(parent=parent.parent())
|
||||
self.reportWizard = parent
|
||||
|
||||
self.setTitle("Entity Page Wizard")
|
||||
self.setMinimumSize(300, 700)
|
||||
|
||||
self.entityName = parent.primaryField
|
||||
self.entityUID = parent.uid
|
||||
|
||||
self.inputNotesEdit = QtWidgets.QPlainTextEdit()
|
||||
self.inputImageEdit = QtWidgets.QLineEdit()
|
||||
self.inputImageEdit.setReadOnly(True)
|
||||
self.addAppendixButton = QtWidgets.QPushButton("Add New Appendix Section")
|
||||
self.removeAppendixButton = QtWidgets.QPushButton("Remove Last Appendix Section")
|
||||
|
||||
self.scrolllayout = QtWidgets.QVBoxLayout()
|
||||
self.scrollwidget = QtWidgets.QWidget()
|
||||
|
||||
self.defaultpic = self.parent().LENTDB.getEntity(self.entityUID).get('Icon')
|
||||
|
||||
summaryLabel = QtWidgets.QLabel(f"Entity {self.entityName} Notes: ")
|
||||
|
||||
imageLabel = QtWidgets.QLabel("Image Path: ")
|
||||
pDirButton = QtWidgets.QPushButton("Select Image...")
|
||||
pDirButton.clicked.connect(self.editPath)
|
||||
pDirButton.setDisabled(True)
|
||||
self.imageCheckBox = QtWidgets.QCheckBox('Add Custom Entity Image')
|
||||
self.imageCheckBox.setChecked(False)
|
||||
self.imageCheckBox.toggled.connect(pDirButton.setEnabled)
|
||||
|
||||
self.addAppendixButton.clicked.connect(self.addSection)
|
||||
self.removeAppendixButton.clicked.connect(self.removeSection)
|
||||
|
||||
self.scrollwidget.setLayout(self.scrolllayout)
|
||||
|
||||
scroll = QtWidgets.QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setWidget(self.scrollwidget)
|
||||
|
||||
hLayout = QtWidgets.QVBoxLayout()
|
||||
hLayout.addWidget(summaryLabel)
|
||||
hLayout.addWidget(self.inputNotesEdit)
|
||||
|
||||
hLayout.addWidget(self.imageCheckBox)
|
||||
hLayout.addWidget(imageLabel)
|
||||
hLayout.addWidget(self.inputImageEdit)
|
||||
hLayout.addWidget(pDirButton)
|
||||
|
||||
hLayout.addItem(QtWidgets.QSpacerItem(10, 30))
|
||||
hLayout.addWidget(self.addAppendixButton)
|
||||
hLayout.addWidget(self.removeAppendixButton)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addLayout(hLayout)
|
||||
layout.addWidget(scroll)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def editPath(self) -> None:
|
||||
selectedPath = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select New Icon',
|
||||
dir=str(Path.home()),
|
||||
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog,
|
||||
filter="Image Files (*.png *.jpg)")[0]
|
||||
if selectedPath != '':
|
||||
self.inputImageEdit.setText(str(Path(selectedPath).absolute()))
|
||||
|
||||
def addSection(self) -> None:
|
||||
appendixWidget = AppendixWidget()
|
||||
self.scrolllayout.addWidget(appendixWidget)
|
||||
|
||||
def removeSection(self) -> None:
|
||||
if numChildren := self.scrolllayout.count():
|
||||
appendixItem = self.scrolllayout.takeAt(numChildren - 1)
|
||||
appendixItem.widget().deleteLater()
|
||||
|
||||
def getData(self):
|
||||
appendixNotes = []
|
||||
if self.inputImageEdit.text() != '' and self.imageCheckBox.isChecked():
|
||||
data = {'EntityNotes': self.inputNotesEdit.toPlainText(), 'EntityImage': self.inputImageEdit.text()}
|
||||
elif 'PNG' in str(self.defaultpic):
|
||||
imagePath = Path(self.reportWizard.reportTempFolder) / f'{str(uuid4())}.png'
|
||||
with open(imagePath, 'wb') as tempFile:
|
||||
tempFile.write(bytearray(self.defaultpic.data()))
|
||||
|
||||
data = {'EntityNotes': self.inputNotesEdit.toPlainText(), 'EntityImage': str(imagePath)}
|
||||
else:
|
||||
if 'svg' not in str(self.defaultpic):
|
||||
# Default picture is an SVG.
|
||||
self.defaultpic = self.reportWizard.parent().RESOURCEHANDLER.getEntityDefaultPicture(
|
||||
self.reportWizard.parent().LENTDB.getEntity(self.entityUID)['Entity Type'])
|
||||
contents = bytearray(self.defaultpic)
|
||||
widthRegex = re.compile(b' width="\d*" ')
|
||||
for widthMatches in widthRegex.findall(self.defaultpic):
|
||||
contents = contents.replace(widthMatches, b' ')
|
||||
heightRegex = re.compile(b' height="\d*" ')
|
||||
for heightMatches in heightRegex.findall(self.defaultpic):
|
||||
contents = contents.replace(heightMatches, b' ')
|
||||
contents = contents.replace(b'<svg ', b'<svg height="150" width="150" ')
|
||||
|
||||
imagePath = Path(self.reportWizard.reportTempFolder) / f'{str(uuid4())}.svg'
|
||||
with open(imagePath, 'wb') as tempFile:
|
||||
tempFile.write(contents)
|
||||
|
||||
image = svg2rlg(imagePath)
|
||||
data = {'EntityNotes': self.inputNotesEdit.toPlainText(), 'EntityImage': image}
|
||||
|
||||
for index in range(self.scrolllayout.count()):
|
||||
childWidget = self.scrolllayout.itemAt(index).widget()
|
||||
appendixDict = {'AppendixEntityNotes': childWidget.inputAppendixNotesEdit.toPlainText(),
|
||||
'AppendixEntityImage': childWidget.inputAppendixImageEdit.text()}
|
||||
appendixNotes.append(appendixDict)
|
||||
return data, appendixNotes
|
||||
|
||||
|
||||
class AppendixWidget(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self) -> None:
|
||||
super(AppendixWidget, self).__init__()
|
||||
appendixWidgetLayout = QtWidgets.QGridLayout()
|
||||
appendixLabelNotes = QtWidgets.QLabel("Entity Notes: ")
|
||||
self.inputAppendixNotesEdit = QtWidgets.QPlainTextEdit()
|
||||
imageAppendixLabel = QtWidgets.QLabel("Image Path: ")
|
||||
appendixButton = QtWidgets.QPushButton("Select Image...")
|
||||
appendixButton.clicked.connect(self.editAppendixPath)
|
||||
self.inputAppendixImageEdit = QtWidgets.QLineEdit()
|
||||
self.inputAppendixImageEdit.setReadOnly(True)
|
||||
appendixWidgetLayout.addWidget(appendixLabelNotes, 0, 0, 1, 1)
|
||||
appendixWidgetLayout.addWidget(self.inputAppendixNotesEdit, 2, 0, 4, 1)
|
||||
appendixWidgetLayout.addWidget(imageAppendixLabel, 7, 0, 1, 1)
|
||||
appendixWidgetLayout.addWidget(self.inputAppendixImageEdit, 9, 0, 1, 1)
|
||||
appendixWidgetLayout.addWidget(appendixButton, 11, 0, 1, 1)
|
||||
self.setLayout(appendixWidgetLayout)
|
||||
self.inputAppendixNotesEdit.setFixedHeight(100)
|
||||
|
||||
def editAppendixPath(self) -> None:
|
||||
selectedPath = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select New Icon',
|
||||
dir=str(Path.home()),
|
||||
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog,
|
||||
filter="Image Files (*.png *.jpg)")[0]
|
||||
|
||||
if selectedPath != '':
|
||||
self.inputAppendixImageEdit.setText(str(Path(selectedPath).absolute()))
|
||||
|
||||
|
||||
class MyDocTemplate(BaseDocTemplate):
|
||||
def __init__(self, filename, **kw):
|
||||
self.allowSplitting = 0
|
||||
@@ -206,14 +570,12 @@ class PDFReport:
|
||||
outgoing_data.append([linkName, childNode, dateCreated, Paragraph(linkNotes)])
|
||||
outgoing_table = Table(data=outgoing_data, style=links_table_style, hAlign="CENTER",
|
||||
colWidths=[140, 140, 140, 140])
|
||||
spacer = Spacer(10, 10)
|
||||
self.elements.append(spacer)
|
||||
else:
|
||||
outgoing_data = [
|
||||
['No Outgoing Links']]
|
||||
outgoing_table = Table(data=outgoing_data, hAlign="CENTER")
|
||||
spacer = Spacer(10, 10)
|
||||
self.elements.append(spacer)
|
||||
spacer = Spacer(10, 10)
|
||||
self.elements.append(spacer)
|
||||
|
||||
# parent UID
|
||||
if incomingLinks:
|
||||
@@ -236,14 +598,12 @@ class PDFReport:
|
||||
incoming_data.append([linkName, parentNode, dateCreated, Paragraph(linkNotes)])
|
||||
incoming_table = Table(data=incoming_data, style=links_table_style, hAlign="CENTER",
|
||||
colWidths=[140, 140, 140, 140])
|
||||
spacer = Spacer(10, 10)
|
||||
self.elements.append(spacer)
|
||||
else:
|
||||
incoming_data = [
|
||||
['No Incoming Links']]
|
||||
incoming_table = Table(data=incoming_data, hAlign="CENTER")
|
||||
spacer = Spacer(10, 10)
|
||||
self.elements.append(spacer)
|
||||
spacer = Spacer(10, 10)
|
||||
self.elements.append(spacer)
|
||||
|
||||
tableParagraph = ParagraphStyle('Report', fontSize=9, justifyBreaks=1, alignment=TA_CENTER,
|
||||
justifyLastLine=0)
|
||||
@@ -296,7 +656,7 @@ class PDFReport:
|
||||
pie.y = 65
|
||||
pie.data = [len(incomingLinks), len(outgoingLinks)]
|
||||
pie.sideLabels = 1
|
||||
pie.labels = ['Incoming: ' + str(len(incomingLinks)), 'Outgoing: ' + str(len(outgoingLinks))]
|
||||
pie.labels = [f'Incoming: {len(incomingLinks)}', f'Outgoing: {len(outgoingLinks)}']
|
||||
pie.slices.strokeWidth = 1
|
||||
if len(incomingLinks) > len(outgoingLinks):
|
||||
pie.slices[0].popout = 5
|
||||
|
||||
@@ -6,26 +6,30 @@ import sys
|
||||
from os import listdir
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
from typing import Union
|
||||
from shutil import move
|
||||
from msgpack import dump, load
|
||||
from typing import Union, Any
|
||||
|
||||
from PySide6 import QtCore, QtWidgets, QtGui
|
||||
from Core.ResourceHandler import StringPropertyInput, FilePropertyInput, SingleChoicePropertyInput, \
|
||||
MultiChoicePropertyInput
|
||||
|
||||
|
||||
class ResolutionManager:
|
||||
|
||||
# Load all resources needed.
|
||||
def __init__(self, mainWindow, messageHandler):
|
||||
self.messageHandler = messageHandler
|
||||
def __init__(self, mainWindow):
|
||||
self.mainWindow = mainWindow
|
||||
self.resolutions = {}
|
||||
# Macro dict item contents: tuple of (resolution, parameter values)
|
||||
self.macros = {}
|
||||
|
||||
def loadResolutionsFromDir(self, directory: Path) -> None:
|
||||
self.loadResolutionsFromDir(
|
||||
Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Core" / "Resolutions" / "Core")
|
||||
|
||||
def loadResolutionsFromDir(self, directory: Path) -> list:
|
||||
resolutionsLoaded = []
|
||||
exceptionsCount = 0
|
||||
for resolution in listdir(directory):
|
||||
resolution = str(resolution)
|
||||
resolutionCategory = "Uncategorized"
|
||||
try:
|
||||
if resolution.endswith('.py'):
|
||||
resolutionName = resolution[:-3]
|
||||
@@ -44,7 +48,7 @@ class ResolutionManager:
|
||||
with contextlib.suppress(AttributeError):
|
||||
resolutionCategory = resClassInst.category
|
||||
if not isinstance(resolutionCategory, str):
|
||||
raise AttributeError()
|
||||
resolutionCategory = "Uncategorized"
|
||||
if self.resolutions.get(resolutionCategory) is None:
|
||||
self.resolutions[resolutionCategory] = {}
|
||||
self.resolutions[resolutionCategory][resNameString] = {'name': resNameString,
|
||||
@@ -55,19 +59,22 @@ class ResolutionManager:
|
||||
'category': resolutionCategory,
|
||||
'resolution': resClass
|
||||
}
|
||||
self.messageHandler.info(f"Loaded Resolution: {resNameString}")
|
||||
self.mainWindow.MESSAGEHANDLER.debug(f"Loaded Resolution: {resNameString}")
|
||||
resolutionsLoaded.append(f'{resolutionCategory}/{resNameString}')
|
||||
except Exception as e:
|
||||
self.messageHandler.error(f"Cannot load resolutions from {str(directory)}" + "\n Info: " + repr(e))
|
||||
self.mainWindow.MESSAGEHANDLER.error(
|
||||
f"Cannot load resolutions from {str(directory)}\n Info: {repr(e)}")
|
||||
exceptionsCount += 1
|
||||
if exceptionsCount > 3:
|
||||
# Will not occur when loading modules with 3 or fewer resolutions, but that should be fine.
|
||||
self.messageHandler.critical("Failed loading too many resolutions to proceed.")
|
||||
self.mainWindow.MESSAGEHANDLER.critical("Failed loading too many resolutions to proceed.")
|
||||
sys.exit(5)
|
||||
return resolutionsLoaded
|
||||
|
||||
def getResolutionParameters(self, resolutionCategory, resolutionNameString):
|
||||
resolutionsList = self.resolutions.get(resolutionCategory)
|
||||
if resolutionsList is not None and resolutionNameString in resolutionsList:
|
||||
return self.resolutions[resolutionCategory][resolutionNameString]['parameters']
|
||||
return dict(self.resolutions[resolutionCategory][resolutionNameString]['parameters'])
|
||||
return None
|
||||
|
||||
def getResolutionOriginTypes(self, resolutionCategoryNameString: str) -> Union[list, None]:
|
||||
@@ -139,8 +146,8 @@ class ResolutionManager:
|
||||
if resolutionName in self.resolutions.get(resolutionCategory):
|
||||
if self.resolutions[resolutionCategory][resolutionName].get('resolution') == '':
|
||||
# If resolution class does not exist locally, then assume it exists on the server.
|
||||
self.mainWindow.executeRemoteResolution(resolutionCategoryNameString, resolutionEntitiesInput,
|
||||
parameters, resolutionUID)
|
||||
self.mainWindow.FCOM.runRemoteResolution(
|
||||
resolutionCategoryNameString, resolutionEntitiesInput, parameters, resolutionUID)
|
||||
# Returning a bool, so we know that the resolution is running on the server.
|
||||
return True
|
||||
resolutionClass = self.resolutions[resolutionCategory][resolutionName]['resolution']()
|
||||
@@ -153,6 +160,21 @@ class ResolutionManager:
|
||||
|
||||
return macroUID
|
||||
|
||||
def renameMacro(self, oldName: str, newName: str) -> bool:
|
||||
if oldName != newName:
|
||||
with self.mainWindow.macrosLock:
|
||||
if newName in self.macros:
|
||||
self.mainWindow.MESSAGEHANDLER.warning('The specified name already exists. '
|
||||
'Macro names must be unique.',
|
||||
popUp=True)
|
||||
return False
|
||||
if oldName not in self.macros:
|
||||
self.mainWindow.MESSAGEHANDLER.error('Attempting to rename a nonexistent macro.', popUp=True)
|
||||
return False
|
||||
oldMacro = self.macros.pop(oldName)
|
||||
self.macros[newName] = oldMacro
|
||||
return True
|
||||
|
||||
def deleteMacro(self, macroUID: str) -> bool:
|
||||
# We don't have to worry about running macros, because the details of the macro are saved in memory.
|
||||
# We do however want to get the thread lock because of potential race conditions.
|
||||
@@ -163,31 +185,519 @@ class ResolutionManager:
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def getMacroFilePath(self):
|
||||
return Path(self.mainWindow.SETTINGS.value("Project/BaseDir")) / "Project Files" / "Project Macros.lsmacros"
|
||||
|
||||
def loadMacros(self):
|
||||
def loadMacros(self) -> None:
|
||||
# Load AFTER we load resolutions.
|
||||
macroFilePath = self.getMacroFilePath()
|
||||
self.macros = self.mainWindow.SETTINGS.value("Program/Macros", {})
|
||||
|
||||
def save(self) -> None:
|
||||
self.mainWindow.SETTINGS.setGlobalValue("Program/Macros", self.macros)
|
||||
|
||||
|
||||
class ResolutionParametersSelector(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, mainWindowObject, resolutionName, properties: dict, includeEntitySelector: list = None,
|
||||
originTypes: list = None, resolutionDescription: str = None,
|
||||
windowTitle: str = None) -> None:
|
||||
super(ResolutionParametersSelector, self).__init__()
|
||||
|
||||
self.setModal(True)
|
||||
if windowTitle is None:
|
||||
windowTitle = f'Resolution Parameter Selector: {resolutionName}'
|
||||
self.setWindowTitle(windowTitle)
|
||||
self.parametersList = []
|
||||
# Have two separate dicts for readability.
|
||||
self.chosenParameters = {}
|
||||
self.properties = properties
|
||||
self.mainWindowObject = mainWindowObject
|
||||
self.resolutionName = resolutionName
|
||||
|
||||
dialogLayout = QtWidgets.QGridLayout()
|
||||
self.setLayout(dialogLayout)
|
||||
self.childWidget = QtWidgets.QTabWidget()
|
||||
dialogLayout.addWidget(self.childWidget, 0, 0, 4, 2)
|
||||
dialogLayout.setRowStretch(0, 1)
|
||||
dialogLayout.setColumnStretch(0, 1)
|
||||
|
||||
if includeEntitySelector is not None and originTypes is not None:
|
||||
entitySelectTab = QtWidgets.QWidget()
|
||||
entitySelectTab.setLayout(QtWidgets.QVBoxLayout())
|
||||
labelText = ""
|
||||
if resolutionDescription is not None:
|
||||
labelText += resolutionDescription + "\n\n"
|
||||
labelText += 'Select the entities to use for this resolution.\nAccepted Origin Types: ' + \
|
||||
', '.join(originTypes)
|
||||
entitySelectTabLabel = QtWidgets.QLabel(labelText)
|
||||
entitySelectTabLabel.setWordWrap(True)
|
||||
entitySelectTabLabel.setMaximumWidth(600)
|
||||
|
||||
entitySelectTabLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
entitySelectTab.layout().addWidget(entitySelectTabLabel)
|
||||
|
||||
self.entitySelector = QtWidgets.QListWidget()
|
||||
self.entitySelector.setSortingEnabled(True)
|
||||
self.entitySelector.addItems(includeEntitySelector)
|
||||
|
||||
self.entitySelector.setSelectionMode(self.entitySelector.SelectionMode.MultiSelection)
|
||||
entitySelectTab.layout().addWidget(self.entitySelector)
|
||||
|
||||
self.childWidget.addTab(entitySelectTab, 'Entities')
|
||||
|
||||
for key in properties:
|
||||
propertyWidget = QtWidgets.QWidget()
|
||||
propertyKeyLayout = QtWidgets.QVBoxLayout()
|
||||
propertyWidget.setLayout(propertyKeyLayout)
|
||||
|
||||
propertyLabel = QtWidgets.QLabel(properties[key].get('description'))
|
||||
propertyLabel.setWordWrap(True)
|
||||
propertyLabel.setMaximumWidth(600)
|
||||
|
||||
propertyLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
propertyKeyLayout.addWidget(propertyLabel)
|
||||
|
||||
propertyType = properties[key].get('type')
|
||||
propertyValue = properties[key].get('value')
|
||||
propertyDefaultValue = properties[key].get('default')
|
||||
|
||||
if propertyType == 'String':
|
||||
propertyInputField = StringPropertyInput(propertyValue, propertyDefaultValue)
|
||||
elif propertyType == 'File':
|
||||
propertyInputField = FilePropertyInput(propertyValue, propertyDefaultValue)
|
||||
elif propertyType == 'SingleChoice':
|
||||
propertyInputField = SingleChoicePropertyInput(propertyValue, propertyDefaultValue)
|
||||
elif propertyType == 'MultiChoice':
|
||||
propertyInputField = MultiChoicePropertyInput(propertyValue, propertyDefaultValue)
|
||||
else:
|
||||
# If value has invalid type, skip to the next property.
|
||||
propertyInputField = None
|
||||
|
||||
if propertyInputField is not None:
|
||||
propertyKeyLayout.addWidget(propertyInputField)
|
||||
|
||||
rememberChoiceCheckbox = QtWidgets.QCheckBox('Remember Choice')
|
||||
rememberChoiceCheckbox.setChecked(False)
|
||||
propertyKeyLayout.addWidget(rememberChoiceCheckbox)
|
||||
propertyKeyLayout.setStretch(1, 1)
|
||||
|
||||
self.childWidget.addTab(propertyWidget, key)
|
||||
self.parametersList.append((key, propertyInputField, rememberChoiceCheckbox))
|
||||
|
||||
nextButton = QtWidgets.QPushButton('Next')
|
||||
nextButton.clicked.connect(self.nextTab)
|
||||
previousButton = QtWidgets.QPushButton('Previous')
|
||||
previousButton.clicked.connect(self.previousTab)
|
||||
acceptButton = QtWidgets.QPushButton('Accept')
|
||||
acceptButton.setAutoDefault(True)
|
||||
acceptButton.setDefault(True)
|
||||
acceptButton.clicked.connect(self.accept)
|
||||
cancelButton = QtWidgets.QPushButton('Cancel')
|
||||
cancelButton.clicked.connect(self.reject)
|
||||
|
||||
dialogLayout.addWidget(previousButton, 4, 0, 1, 1)
|
||||
dialogLayout.addWidget(nextButton, 4, 1, 1, 1)
|
||||
dialogLayout.addWidget(cancelButton, 5, 0, 1, 1)
|
||||
dialogLayout.addWidget(acceptButton, 5, 1, 1, 1)
|
||||
|
||||
def nextTab(self):
|
||||
currentIndex = self.childWidget.currentIndex()
|
||||
if currentIndex < self.childWidget.count():
|
||||
self.childWidget.setCurrentIndex(currentIndex + 1)
|
||||
|
||||
def previousTab(self):
|
||||
currentIndex = self.childWidget.currentIndex()
|
||||
if currentIndex > 0:
|
||||
self.childWidget.setCurrentIndex(currentIndex - 1)
|
||||
|
||||
def accept(self) -> None:
|
||||
savedParameters = {}
|
||||
for resolutionParameterName, resolutionParameterInput, resolutionParameterRemember in self.parametersList:
|
||||
value = resolutionParameterInput.getValue()
|
||||
if value == '':
|
||||
msgBox = QtWidgets.QMessageBox()
|
||||
msgBox.setModal(True)
|
||||
QtWidgets.QMessageBox.warning(msgBox,
|
||||
"Not all parameters were filled in",
|
||||
"Some of the required parameters for the resolution have been left blank."
|
||||
" Please fill them in before proceeding.")
|
||||
return
|
||||
self.chosenParameters[resolutionParameterName] = value
|
||||
|
||||
if resolutionParameterRemember.isChecked():
|
||||
savedParameters[resolutionParameterName] = value
|
||||
|
||||
# Only save parameters after we verify that everything is filled in properly.
|
||||
for savedParameter in savedParameters:
|
||||
if self.properties[savedParameter].get('global') is True:
|
||||
self.mainWindowObject.SETTINGS.setGlobalValue(
|
||||
f'Resolutions/Global/Parameters/{savedParameter}',
|
||||
savedParameters[savedParameter],
|
||||
)
|
||||
else:
|
||||
self.mainWindowObject.SETTINGS.setGlobalValue(
|
||||
f'Resolutions/{self.resolutionName}/{savedParameter}',
|
||||
savedParameters[savedParameter],
|
||||
)
|
||||
|
||||
super(ResolutionParametersSelector, self).accept()
|
||||
|
||||
|
||||
class ResolutionSearchResultsList(QtWidgets.QListWidget):
|
||||
|
||||
def __init__(self, mainWindowObject):
|
||||
super(ResolutionSearchResultsList, self).__init__()
|
||||
self.mainWindow = mainWindowObject
|
||||
self.setSortingEnabled(True)
|
||||
|
||||
def mouseDoubleClickEvent(self, event: QtGui.QMouseEvent) -> None:
|
||||
super(ResolutionSearchResultsList, self).mouseDoubleClickEvent(event)
|
||||
resItem = self.itemAt(event.pos())
|
||||
if resItem is None or '/' not in resItem.text():
|
||||
return
|
||||
self.mainWindow.centralWidget().tabbedPane.getCurrentScene().clearSelection()
|
||||
self.mainWindow.runResolution(resItem.text())
|
||||
|
||||
|
||||
class FindResolutionDialog(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, parent, entityList: list, resolutionDict: dict):
|
||||
super(FindResolutionDialog, self).__init__()
|
||||
self.entities = entityList
|
||||
self.resolutions = resolutionDict
|
||||
self.setModal(True)
|
||||
self.setWindowTitle('Find Resolutions')
|
||||
|
||||
dialogLayout = QtWidgets.QGridLayout()
|
||||
self.setLayout(dialogLayout)
|
||||
|
||||
descriptionLabel = QtWidgets.QLabel("Find Resolutions based on their parameters.")
|
||||
descriptionLabel.setWordWrap(True)
|
||||
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
|
||||
|
||||
originLabel = QtWidgets.QLabel("Origin Entity:")
|
||||
self.originDropDown = QtWidgets.QComboBox()
|
||||
self.originDropDown.addItem('Any')
|
||||
self.originDropDown.addItems(entityList)
|
||||
self.originDropDown.addItem('*')
|
||||
dialogLayout.addWidget(originLabel, 1, 0, 1, 1)
|
||||
dialogLayout.addWidget(self.originDropDown, 1, 1, 1, 1)
|
||||
|
||||
targetLabel = QtWidgets.QLabel("Target Entity:")
|
||||
self.targetDropDown = QtWidgets.QComboBox()
|
||||
self.targetDropDown.addItem('Any')
|
||||
self.targetDropDown.addItems(entityList)
|
||||
self.targetDropDown.addItem('*')
|
||||
dialogLayout.addWidget(targetLabel, 2, 0, 1, 1)
|
||||
dialogLayout.addWidget(self.targetDropDown, 2, 1, 1, 1)
|
||||
|
||||
keywordsLabel = QtWidgets.QLabel("Keywords:")
|
||||
self.keywordsWidget = QtWidgets.QLineEdit()
|
||||
self.keywordsWidget.setToolTip("Add keywords separated by spaces.\nKeywords are checked against the "
|
||||
"resolutions' titles and descriptions.")
|
||||
dialogLayout.addWidget(keywordsLabel, 3, 0, 1, 2)
|
||||
dialogLayout.addWidget(self.keywordsWidget, 4, 0, 1, 2)
|
||||
|
||||
resultsLabel = QtWidgets.QLabel("Matches:")
|
||||
self.resultsWidget = ResolutionSearchResultsList(parent)
|
||||
self.resultsWidget.addItem('Click "Search" to display results')
|
||||
dialogLayout.addWidget(resultsLabel, 5, 0, 1, 2)
|
||||
dialogLayout.addWidget(self.resultsWidget, 6, 0, 2, 2)
|
||||
|
||||
self.searchButton = QtWidgets.QPushButton("Search")
|
||||
self.searchButton.clicked.connect(self.search)
|
||||
self.closeButton = QtWidgets.QPushButton("Close")
|
||||
self.closeButton.clicked.connect(self.accept)
|
||||
dialogLayout.addWidget(self.closeButton, 8, 0, 1, 1)
|
||||
dialogLayout.addWidget(self.searchButton, 8, 1, 1, 1)
|
||||
|
||||
def search(self):
|
||||
self.resultsWidget.clear()
|
||||
target = self.targetDropDown.currentText()
|
||||
validResolutions = []
|
||||
for category in self.resolutions:
|
||||
for resolution in self.resolutions[category]:
|
||||
if target == 'Any':
|
||||
validResolutions.append(f'{category}/{resolution}')
|
||||
|
||||
elif target in self.resolutions[category][resolution]['originTypes']:
|
||||
validResolutions.append(f'{category}/{resolution}')
|
||||
origin = self.originDropDown.currentText()
|
||||
if origin != 'Any':
|
||||
for category in self.resolutions:
|
||||
for resolution in self.resolutions[category]:
|
||||
if origin not in self.resolutions[category][resolution]['originTypes']:
|
||||
with contextlib.suppress(KeyError):
|
||||
validResolutions.remove(f'{str(category)}/{str(resolution)}')
|
||||
# Try to see if any of the keywords are a substring of the name or description of any resolution.
|
||||
keywordFilter = self.keywordsWidget.text().strip()
|
||||
if keywordFilter != '':
|
||||
wordsToFind = keywordFilter.split(' ')
|
||||
for category in self.resolutions:
|
||||
for resolution in self.resolutions[category]:
|
||||
titleText = self.resolutions[category][resolution]['name']
|
||||
descriptionText = self.resolutions[category][resolution]['description']
|
||||
for keyword in wordsToFind:
|
||||
if keyword not in titleText and keyword not in descriptionText:
|
||||
with contextlib.suppress(KeyError):
|
||||
validResolutions.remove(f'{str(category)}/{str(resolution)}')
|
||||
for result in validResolutions:
|
||||
self.resultsWidget.addItem(result)
|
||||
|
||||
|
||||
class ResolutionExecutorThread(QtCore.QThread):
|
||||
sig = QtCore.Signal(str, list, str)
|
||||
sigStr = QtCore.Signal(str, str, str)
|
||||
sigError = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, resolution: str, resolutionArgument: list, resolutionParameters: dict,
|
||||
mainWindowObject, uid: str):
|
||||
super().__init__()
|
||||
self.resolution = resolution
|
||||
self.resolutionArgument = resolutionArgument
|
||||
self.resolutionParameters = resolutionParameters
|
||||
self.mainWindow = mainWindowObject
|
||||
self.return_results = True
|
||||
self.uid = uid
|
||||
self.done = False
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
with open(macroFilePath, "rb") as macroFile:
|
||||
self.macros = load(macroFile)
|
||||
except ValueError:
|
||||
# If the Macros file is empty or contains invalid input, ignore it.
|
||||
pass
|
||||
except FileNotFoundError:
|
||||
# Create new placeholder notes file if it doesn't exist.
|
||||
try:
|
||||
macroFilePath.touch(0o700, exist_ok=False)
|
||||
self.mainWindow.MESSAGEHANDLER.info('Created new Macros file.')
|
||||
except FileExistsError:
|
||||
self.mainWindow.MESSAGEHANDLER.error('Race condition occurred while trying to create Macros file.')
|
||||
ret = self.mainWindow.RESOLUTIONMANAGER.executeResolution(self.resolution,
|
||||
self.resolutionArgument,
|
||||
self.resolutionParameters,
|
||||
self.uid)
|
||||
if ret is None:
|
||||
self.sigError.emit(f'Resolution {self.resolution} failed during run.')
|
||||
elif isinstance(ret, bool):
|
||||
# Resolution is running on the server, we do not have results right now.
|
||||
ret = None
|
||||
except Exception as e:
|
||||
self.sigError.emit(f'Resolution {self.resolution} failed during run: {str(e)}')
|
||||
ret = None
|
||||
|
||||
def save(self):
|
||||
macroFilePath = self.getMacroFilePath()
|
||||
macroFilePathTmp = macroFilePath.with_suffix(f'{macroFilePath.suffix}.tmp')
|
||||
# If the resolution is ran on the server or there is a problem, don't emit signal.
|
||||
if ret is not None and self.return_results:
|
||||
if isinstance(ret, str):
|
||||
self.sigStr.emit(self.resolution, ret, self.uid)
|
||||
else:
|
||||
self.sig.emit(self.resolution, ret, self.uid)
|
||||
self.done = True
|
||||
|
||||
|
||||
class MacroDialog(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, mainWindowObject):
|
||||
super(MacroDialog, self).__init__()
|
||||
self.mainWindowObject = mainWindowObject
|
||||
self.setModal(True)
|
||||
|
||||
self.resolutionList = []
|
||||
for category in mainWindowObject.RESOLUTIONMANAGER.getResolutionCategories():
|
||||
self.resolutionList.extend(
|
||||
f'{category}/{resolution}'
|
||||
for resolution in mainWindowObject.RESOLUTIONMANAGER.getResolutionsInCategory(category)
|
||||
)
|
||||
self.resolutionList.sort()
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
macroLabel = QtWidgets.QLabel("This is a list of all currently configured Macros.\n"
|
||||
"Click on a Macro to view the Resolutions included in it.")
|
||||
macroLabel.setWordWrap(True)
|
||||
self.macroTree = MacroTree(self, mainWindowObject)
|
||||
self.macroTree.setSelectionMode(self.macroTree.SelectionMode.ExtendedSelection)
|
||||
self.macroTree.setSelectionBehavior(self.macroTree.SelectionBehavior.SelectRows)
|
||||
self.macroTree.setHeaderLabels(['Macro UID', 'Delete'])
|
||||
self.macroTree.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.macroTree.setSortingEnabled(False)
|
||||
# Stretch the first column, since it contains the primary field.
|
||||
self.macroTree.header().setStretchLastSection(False)
|
||||
self.macroTree.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch)
|
||||
|
||||
buttonsWidget = QtWidgets.QWidget()
|
||||
buttonsWidgetLayout = QtWidgets.QHBoxLayout()
|
||||
buttonsWidget.setLayout(buttonsWidgetLayout)
|
||||
closeButton = QtWidgets.QPushButton('Close')
|
||||
closeButton.clicked.connect(self.reject)
|
||||
addMacroButton = QtWidgets.QPushButton('Create New Macro')
|
||||
addMacroButton.clicked.connect(self.createMacro)
|
||||
runSelectedButton = QtWidgets.QPushButton('Run Selected Macros')
|
||||
runSelectedButton.clicked.connect(self.accept)
|
||||
buttonsWidgetLayout.addWidget(closeButton)
|
||||
buttonsWidgetLayout.addWidget(addMacroButton)
|
||||
buttonsWidgetLayout.addWidget(runSelectedButton)
|
||||
|
||||
layout.addWidget(macroLabel)
|
||||
layout.addWidget(self.macroTree)
|
||||
layout.addWidget(buttonsWidget)
|
||||
self.updateMacroTree()
|
||||
self.setBaseSize(1000, 1000)
|
||||
|
||||
def updateMacroTree(self) -> None:
|
||||
self.macroTree.clear()
|
||||
with self.mainWindowObject.macrosLock:
|
||||
allMacros = self.mainWindowObject.RESOLUTIONMANAGER.macros
|
||||
|
||||
for macro in allMacros:
|
||||
newMacro = MacroTreeItem(macro, allMacros[macro])
|
||||
self.macroTree.addTopLevelItem(newMacro)
|
||||
self.macroTree.setItemWidget(newMacro, 1, newMacro.deleteButton)
|
||||
|
||||
def createMacro(self) -> None:
|
||||
createMacroDialog = MacroCreatorDialog(self.resolutionList)
|
||||
if createMacroDialog.exec():
|
||||
macroResolutionsList = []
|
||||
numberOfResolutionsSelected = createMacroDialog.createList.count()
|
||||
for itemIndex in range(numberOfResolutionsSelected):
|
||||
itemText = createMacroDialog.createList.item(itemIndex).text()
|
||||
resolutionCategory, resolutionName = itemText.split('/', 1)
|
||||
rParameters = self.mainWindowObject.RESOLUTIONMANAGER.getResolutionParameters(resolutionCategory,
|
||||
resolutionName)
|
||||
if rParameters is None:
|
||||
message = f'Resolution parameters not found for resolution: {resolutionName}'
|
||||
self.mainWindowObject.MESSAGEHANDLER.error(message, popUp=True, exc_info=False)
|
||||
self.mainWindowObject.setStatus(f'{message}, Macro creation aborted.')
|
||||
return
|
||||
|
||||
resolutionParameterValues = self.mainWindowObject.popParameterValuesAndReturnSpecified(resolutionName,
|
||||
rParameters)
|
||||
|
||||
if rParameters:
|
||||
parameterSelector = ResolutionParametersSelector(
|
||||
self.mainWindowObject, resolutionName, rParameters,
|
||||
windowTitle=f'[{str(itemIndex + 1)}/{str(numberOfResolutionsSelected)}] Select Parameter '
|
||||
f'values for Resolution: {resolutionName}')
|
||||
if parameterSelector.exec():
|
||||
resolutionParameterValues.update(parameterSelector.chosenParameters)
|
||||
else:
|
||||
self.mainWindowObject.MESSAGEHANDLER.info('Macro creation aborted.')
|
||||
self.mainWindowObject.setStatus('Macro creation aborted.')
|
||||
return
|
||||
|
||||
macroResolutionsList.append((itemText, resolutionParameterValues))
|
||||
self.mainWindowObject.RESOLUTIONMANAGER.createMacro(macroResolutionsList)
|
||||
self.updateMacroTree()
|
||||
self.mainWindowObject.setStatus('New Macro Created.')
|
||||
self.mainWindowObject.MESSAGEHANDLER.info('New Macro Created.')
|
||||
|
||||
def accept(self) -> None:
|
||||
super(MacroDialog, self).accept()
|
||||
|
||||
|
||||
class MacroTree(QtWidgets.QTreeWidget):
|
||||
|
||||
def __init__(self, parent, mainWindowObject):
|
||||
super(MacroTree, self).__init__(parent=parent)
|
||||
self.mainWindowObject = mainWindowObject
|
||||
|
||||
def deleteMacro(self, treeEntry: QtWidgets.QTreeWidgetItem):
|
||||
index = self.indexOfTopLevelItem(treeEntry)
|
||||
uid = treeEntry.text(0)
|
||||
self.mainWindowObject.RESOLUTIONMANAGER.deleteMacro(uid)
|
||||
self.takeTopLevelItem(index)
|
||||
|
||||
|
||||
class MacroTreeItem(QtWidgets.QTreeWidgetItem):
|
||||
|
||||
def __init__(self, uid: str, resolutionList: list):
|
||||
super(MacroTreeItem, self).__init__()
|
||||
self.setText(0, uid)
|
||||
self.uid = uid
|
||||
self.setFlags(QtCore.Qt.ItemFlag.ItemIsEditable | self.flags())
|
||||
|
||||
self.deleteButton = QtWidgets.QPushButton('X')
|
||||
self.deleteButton.clicked.connect(self.removeSelf)
|
||||
|
||||
for resolution in resolutionList:
|
||||
resolutionItem = QtWidgets.QTreeWidgetItem()
|
||||
resolutionItem.setText(0, f'Resolution: {resolution[0]}')
|
||||
self.addChild(resolutionItem)
|
||||
|
||||
def setData(self, column: int, role: int, value: Any):
|
||||
if self.treeWidget() is None:
|
||||
# Happens during initialization
|
||||
super().setData(column, role, value)
|
||||
|
||||
elif self.treeWidget().mainWindowObject.RESOLUTIONMANAGER.renameMacro(self.uid, value):
|
||||
super().setData(column, role, value)
|
||||
self.uid = value
|
||||
|
||||
def removeSelf(self):
|
||||
self.treeWidget().deleteMacro(self)
|
||||
|
||||
|
||||
class MacroCreatorDialog(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, resolutionsWithCategoriesList: list):
|
||||
super(MacroCreatorDialog, self).__init__()
|
||||
self.setWindowTitle('Create new Macro')
|
||||
|
||||
self.viewList = QtWidgets.QListWidget()
|
||||
for resolution in resolutionsWithCategoriesList:
|
||||
viewItem = QtWidgets.QListWidgetItem(resolution)
|
||||
self.viewList.addItem(viewItem)
|
||||
self.viewList.sortItems()
|
||||
self.viewList.setSelectionMode(self.viewList.SelectionMode.ExtendedSelection)
|
||||
|
||||
buttonsAddRemoveWidget = QtWidgets.QWidget()
|
||||
buttonsAddRemoveWidgetLayout = QtWidgets.QVBoxLayout()
|
||||
buttonAdd = QtWidgets.QPushButton('>')
|
||||
buttonAdd.clicked.connect(self.addSelectedToMacro)
|
||||
buttonRemove = QtWidgets.QPushButton('<')
|
||||
buttonRemove.clicked.connect(self.removeSelectedFromMacro)
|
||||
buttonsAddRemoveWidget.setLayout(buttonsAddRemoveWidgetLayout)
|
||||
buttonsAddRemoveWidgetLayout.addWidget(buttonAdd)
|
||||
buttonsAddRemoveWidgetLayout.addWidget(buttonRemove)
|
||||
self.createList = QtWidgets.QListWidget()
|
||||
buttonsRearrangeWidget = QtWidgets.QWidget()
|
||||
buttonsRearrangeWidgetLayout = QtWidgets.QVBoxLayout()
|
||||
buttonsRearrangeWidget.setLayout(buttonsRearrangeWidgetLayout)
|
||||
buttonMoveUp = QtWidgets.QPushButton('^')
|
||||
buttonMoveUp.clicked.connect(self.shiftSelectedUp)
|
||||
buttonMoveDown = QtWidgets.QPushButton('v')
|
||||
buttonMoveDown.clicked.connect(self.shiftSelectedDown)
|
||||
buttonsRearrangeWidgetLayout.addWidget(buttonMoveUp)
|
||||
buttonsRearrangeWidgetLayout.addWidget(buttonMoveDown)
|
||||
allResolutionsLabel = QtWidgets.QLabel('All Resolutions')
|
||||
allResolutionsLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
selectedResolutionsLabel = QtWidgets.QLabel('Selected Resolutions')
|
||||
selectedResolutionsLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
confirmButton = QtWidgets.QPushButton('Confirm')
|
||||
confirmButton.clicked.connect(self.accept)
|
||||
cancelButton = QtWidgets.QPushButton('Cancel')
|
||||
cancelButton.clicked.connect(self.reject)
|
||||
|
||||
macroCreateLayout = QtWidgets.QGridLayout()
|
||||
self.setLayout(macroCreateLayout)
|
||||
macroCreateLayout.addWidget(self.viewList, 1, 0, 1, 5)
|
||||
macroCreateLayout.addWidget(buttonsAddRemoveWidget, 1, 5, 1, 1)
|
||||
macroCreateLayout.addWidget(self.createList, 1, 6, 1, 5)
|
||||
macroCreateLayout.addWidget(buttonsRearrangeWidget, 1, 11, 1, 1)
|
||||
macroCreateLayout.addWidget(allResolutionsLabel, 0, 0, 1, 5)
|
||||
macroCreateLayout.addWidget(selectedResolutionsLabel, 0, 6, 1, 5)
|
||||
macroCreateLayout.addWidget(cancelButton, 2, 1, 1, 3)
|
||||
macroCreateLayout.addWidget(confirmButton, 2, 7, 1, 3)
|
||||
self.setBaseSize(1000, 700)
|
||||
|
||||
def addSelectedToMacro(self):
|
||||
for selectedItem in self.viewList.selectedItems():
|
||||
self.createList.addItem(QtWidgets.QListWidgetItem(selectedItem.text()))
|
||||
|
||||
def removeSelectedFromMacro(self):
|
||||
for selectedItem in self.createList.selectedItems():
|
||||
self.createList.takeItem(self.createList.row(selectedItem))
|
||||
|
||||
def shiftSelectedUp(self):
|
||||
with contextlib.suppress(IndexError):
|
||||
selectedItemIndex = self.createList.row(self.createList.selectedItems()[0])
|
||||
if selectedItemIndex != 0:
|
||||
currentItem = self.createList.takeItem(selectedItemIndex)
|
||||
self.createList.insertItem(selectedItemIndex - 1, currentItem)
|
||||
currentItem.setSelected(True)
|
||||
|
||||
def shiftSelectedDown(self):
|
||||
with contextlib.suppress(IndexError):
|
||||
selectedItemIndex = self.createList.row(self.createList.selectedItems()[0])
|
||||
currentItem = self.createList.takeItem(selectedItemIndex)
|
||||
self.createList.insertItem(selectedItemIndex + 1, currentItem)
|
||||
currentItem.setSelected(True)
|
||||
|
||||
with open(macroFilePathTmp, "wb") as macroFile:
|
||||
dump(self.macros, macroFile)
|
||||
move(macroFilePathTmp, macroFilePath)
|
||||
|
||||
@@ -10,7 +10,7 @@ class ASNToCIDR:
|
||||
originTypes = {'Autonomous System'}
|
||||
|
||||
# A set of entities that could be the result of this resolution.
|
||||
resultTypes = {'Network'}
|
||||
resultTypes = {'Network', 'Company', 'Organization', 'Phrase'}
|
||||
|
||||
# A dictionary of properties for this resolution. The key is the property name,
|
||||
# the value is the property attributes. The type of input expected from the user is determined by the
|
||||
@@ -46,18 +46,13 @@ class ASNToCIDR:
|
||||
cidrWithOutPrefix = split_string[0]
|
||||
prefix = split_string[1]
|
||||
index_of_child = len(returnResult)
|
||||
returnResult.append([{'IP Address': cidrWithOutPrefix,
|
||||
'Range': prefix,
|
||||
'Entity Type': 'Network'},
|
||||
{uid: {'Resolution': 'ASN to CIDR', 'Notes': ''}}])
|
||||
returnResult.append(
|
||||
[{'Phrase': network['description'], 'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'CIDR Description', 'Notes': ''}}])
|
||||
returnResult.append(
|
||||
[{'Organization Name': network['source'], 'Entity Type': 'Organization'},
|
||||
{index_of_child: {'Resolution': 'ASN Registry', 'Notes': ''}}])
|
||||
returnResult.append(
|
||||
[{'Company Name': network['maintainer'], 'Entity Type': 'Company'},
|
||||
{index_of_child: {'Resolution': 'Company Name', 'Notes': ''}}])
|
||||
returnResult.extend(([{'IP Address': cidrWithOutPrefix, 'Range': prefix, 'Entity Type': 'Network'},
|
||||
{uid: {'Resolution': 'ASN to CIDR', 'Notes': ''}}],
|
||||
[{'Phrase': network['description'], 'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'CIDR Description', 'Notes': ''}}],
|
||||
[{'Organization Name': network['source'], 'Entity Type': 'Organization'},
|
||||
{index_of_child: {'Resolution': 'ASN Registry', 'Notes': ''}}],
|
||||
[{'Company Name': network['maintainer'], 'Entity Type': 'Company'},
|
||||
{index_of_child: {'Resolution': 'Company Name', 'Notes': ''}}]))
|
||||
|
||||
return returnResult
|
||||
|
||||
@@ -44,12 +44,14 @@ class AffiliateCodesExtractor:
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
from playwright.sync_api import sync_playwright, TimeoutError, Error
|
||||
from bs4 import BeautifulSoup
|
||||
from pathlib import Path
|
||||
import urllib
|
||||
import tldextract
|
||||
import re
|
||||
|
||||
playwrightPath = Path(parameters['Playwright Firefox'])
|
||||
returnResults = []
|
||||
visitExternal = True if parameters['Visit External Links'] == 'Yes' else False
|
||||
visitExternal = parameters['Visit External Links'] == 'Yes'
|
||||
|
||||
# Numbers less than zero are the same as zero, but we should try to prevent overflows.
|
||||
try:
|
||||
@@ -133,60 +135,58 @@ class AffiliateCodesExtractor:
|
||||
linksInLinkHref = soupContents.find_all('link')
|
||||
for tag in linksInLinkHref:
|
||||
newLink = tag.get('href', None)
|
||||
if newLink is not None:
|
||||
if newLink.startswith('http'):
|
||||
newLink = newLink.split('#')[0]
|
||||
newDepth = depth - 1
|
||||
if domain in newLink and newLink not in exploredDepth and newDepth > 0:
|
||||
exploredDepth.add(newLink)
|
||||
extractCodes(currentUID, newLink, newDepth)
|
||||
if newLink is not None and newLink.startswith('http'):
|
||||
newLink = newLink.split('#')[0]
|
||||
newDepth = depth - 1
|
||||
if domain in newLink and newLink not in exploredDepth and newDepth > 0:
|
||||
exploredDepth.add(newLink)
|
||||
extractCodes(currentUID, newLink, newDepth)
|
||||
|
||||
linksInAHref = soupContents.find_all('a')
|
||||
for tag in linksInAHref:
|
||||
newLink = tag.get('href', None)
|
||||
if newLink is not None:
|
||||
if newLink.startswith('http'):
|
||||
newLink = newLink.split('#')[0]
|
||||
newDepth = depth - 1
|
||||
if domain in newLink:
|
||||
redirLinks = redirectRegex.findall(newLink)
|
||||
if 'redirect' in newLink and len(redirLinks) > 0:
|
||||
newLink = str(urllib.parse.unquote(redirLinks[0]))[2:]
|
||||
if newLink not in exploredForeign:
|
||||
exploredForeign.add(newLink)
|
||||
if visitExternal:
|
||||
for _ in range(3):
|
||||
try:
|
||||
page.goto(newLink, wait_until="networkidle", timeout=10000)
|
||||
GetAffiliateCodes(currentUID, page.url)
|
||||
break
|
||||
except TimeoutError:
|
||||
pass
|
||||
except Error:
|
||||
break
|
||||
else:
|
||||
GetAffiliateCodes(currentUID, newLink)
|
||||
else:
|
||||
if newLink not in exploredDepth and newDepth > 0:
|
||||
exploredDepth.add(newLink)
|
||||
extractCodes(currentUID, newLink, newDepth)
|
||||
elif newLink not in exploredForeign:
|
||||
exploredForeign.add(newLink)
|
||||
if visitExternal:
|
||||
for _ in range(3):
|
||||
try:
|
||||
page.goto(newLink, wait_until="networkidle", timeout=10000)
|
||||
GetAffiliateCodes(currentUID, page.url)
|
||||
break
|
||||
except TimeoutError:
|
||||
pass
|
||||
except Error:
|
||||
break
|
||||
if newLink is not None and newLink.startswith('http'):
|
||||
newLink = newLink.split('#')[0]
|
||||
newDepth = depth - 1
|
||||
if domain in newLink:
|
||||
redirLinks = redirectRegex.findall(newLink)
|
||||
if 'redirect' in newLink and len(redirLinks) > 0:
|
||||
newLink = str(urllib.parse.unquote(redirLinks[0]))[2:]
|
||||
if newLink not in exploredForeign:
|
||||
exploredForeign.add(newLink)
|
||||
if visitExternal:
|
||||
for _ in range(3):
|
||||
try:
|
||||
page.goto(newLink, wait_until="networkidle", timeout=10000)
|
||||
GetAffiliateCodes(currentUID, page.url)
|
||||
break
|
||||
except TimeoutError:
|
||||
pass
|
||||
except Error:
|
||||
break
|
||||
else:
|
||||
GetAffiliateCodes(currentUID, newLink)
|
||||
else:
|
||||
if newLink not in exploredDepth and newDepth > 0:
|
||||
exploredDepth.add(newLink)
|
||||
extractCodes(currentUID, newLink, newDepth)
|
||||
elif newLink not in exploredForeign:
|
||||
exploredForeign.add(newLink)
|
||||
if visitExternal:
|
||||
for _ in range(3):
|
||||
try:
|
||||
page.goto(newLink, wait_until="networkidle", timeout=10000)
|
||||
GetAffiliateCodes(currentUID, page.url)
|
||||
break
|
||||
except TimeoutError:
|
||||
pass
|
||||
except Error:
|
||||
break
|
||||
else:
|
||||
GetAffiliateCodes(currentUID, newLink)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.firefox.launch()
|
||||
browser = p.firefox.launch(executable_path=playwrightPath)
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0'
|
||||
@@ -197,7 +197,7 @@ class AffiliateCodesExtractor:
|
||||
if url is None:
|
||||
continue
|
||||
if not url.startswith('http://') and not url.startswith('https://'):
|
||||
url = 'http://' + url
|
||||
url = f'http://{url}'
|
||||
domain = tldextract.extract(url).fqdn
|
||||
extractCodes(uid, url, maxDepth)
|
||||
browser.close()
|
||||
|
||||
@@ -11,6 +11,7 @@ class CertificateInfo:
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import contextlib
|
||||
import ssl
|
||||
import socket
|
||||
|
||||
@@ -54,21 +55,18 @@ class CertificateInfo:
|
||||
'Entity Type': 'Domain'},
|
||||
{uid: {'Resolution': 'Certificate Subject Common Name',
|
||||
'Notes': ''}}])
|
||||
|
||||
elif subjectAttributeInnerKey == 'streetAddress':
|
||||
streetAddr = subjectAttributeInnerValue
|
||||
elif subjectAttributeInnerKey == 'countryName':
|
||||
subjectCountry = subjectAttributeInnerValue
|
||||
elif subjectAttributeInnerKey == 'postalCode':
|
||||
postalCode = subjectAttributeInnerValue
|
||||
elif subjectAttributeInnerKey == 'localityName':
|
||||
locality = subjectAttributeInnerValue
|
||||
|
||||
elif subjectAttributeInnerKey == 'serialNumber':
|
||||
subjectSerial = subjectAttributeInnerValue
|
||||
elif subjectAttributeInnerKey == 'organizationName':
|
||||
subjectName = subjectAttributeInnerValue
|
||||
|
||||
elif subjectAttributeInnerKey == 'postalCode':
|
||||
postalCode = subjectAttributeInnerValue
|
||||
elif subjectAttributeInnerKey == 'serialNumber':
|
||||
subjectSerial = subjectAttributeInnerValue
|
||||
elif subjectAttributeInnerKey == 'streetAddress':
|
||||
streetAddr = subjectAttributeInnerValue
|
||||
subjectIndex = None
|
||||
if subjectName is not None:
|
||||
subjectIndex = len(returnResults)
|
||||
@@ -141,45 +139,33 @@ class CertificateInfo:
|
||||
'Notes': ''}}])
|
||||
|
||||
# Domain names included in the certificate.
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
for altNameAttribute in websiteCertificate['subjectAltName']:
|
||||
returnResults.append([{'Domain Name': altNameAttribute[1],
|
||||
'Entity Type': 'Domain'},
|
||||
{uid: {'Resolution': 'Certificate Subject Alternate Name',
|
||||
'Notes': ''}}])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# OCSP URLs. Often just one.
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
for ocsp in websiteCertificate['OCSP']:
|
||||
returnResults.append([{'URL': ocsp,
|
||||
'Entity Type': 'Website'},
|
||||
{uid: {'Resolution': 'Certificate OCSP URL',
|
||||
'Notes': ''}}])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# CA Issuer URL
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
for caIssuer in websiteCertificate['caIssuers']:
|
||||
returnResults.append([{'URL': caIssuer,
|
||||
'Entity Type': 'Website'},
|
||||
{uid: {'Resolution': 'Certificate Authority Issuer URL',
|
||||
'Notes': ''}}])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# CRL URLs
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
for crlDistributionPoint in websiteCertificate['crlDistributionPoints']:
|
||||
returnResults.append([{'URL': crlDistributionPoint,
|
||||
'Entity Type': 'Website'},
|
||||
{uid: {'Resolution': 'Certificate Authority Revocation List URL',
|
||||
'Notes': ''}}])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Issuer information
|
||||
orgName = None
|
||||
orgCommonName = None
|
||||
@@ -192,21 +178,20 @@ class CertificateInfo:
|
||||
for issuerAttributeInner in issuerAttributeOuter:
|
||||
issuerAttributeInnerKey = issuerAttributeInner[0]
|
||||
issuerAttributeInnerValue = issuerAttributeInner[1]
|
||||
if issuerAttributeInnerKey == 'organizationName':
|
||||
orgName = issuerAttributeInnerValue
|
||||
elif issuerAttributeInnerKey == 'commonName':
|
||||
if issuerAttributeInnerKey == 'commonName':
|
||||
orgCommonName = issuerAttributeInnerValue
|
||||
elif issuerAttributeInnerKey == 'countryName':
|
||||
orgCountry = issuerAttributeInnerValue
|
||||
elif issuerAttributeInnerKey == 'postalCode':
|
||||
orgPostal = issuerAttributeInnerValue
|
||||
elif issuerAttributeInnerKey == 'localityName':
|
||||
orgLocality = issuerAttributeInnerValue
|
||||
elif issuerAttributeInnerKey == 'stateOrProvinceName':
|
||||
orgStateOrProvince = issuerAttributeInnerValue
|
||||
elif issuerAttributeInnerKey == 'orgUnitName':
|
||||
orgUnitName = issuerAttributeInnerValue
|
||||
|
||||
elif issuerAttributeInnerKey == 'organizationName':
|
||||
orgName = issuerAttributeInnerValue
|
||||
elif issuerAttributeInnerKey == 'postalCode':
|
||||
orgPostal = issuerAttributeInnerValue
|
||||
elif issuerAttributeInnerKey == 'stateOrProvinceName':
|
||||
orgStateOrProvince = issuerAttributeInnerValue
|
||||
issuerIndex = None
|
||||
if orgName is not None:
|
||||
issuerIndex = len(returnResults)
|
||||
|
||||
@@ -22,7 +22,8 @@ class ContainsPhrase:
|
||||
'value': ''},
|
||||
'Case Sensitive': {'description': 'Do you want the phrase to be case sensitive?',
|
||||
'type': 'SingleChoice',
|
||||
'value': {'Yes', 'No'}
|
||||
'value': {'Yes', 'No'},
|
||||
'default': 'Yes'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
261
Core/Resolutions/Core/CryptoAddressExtractor.py
Normal file
261
Core/Resolutions/Core/CryptoAddressExtractor.py
Normal file
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Credit to @cyb_detective:
|
||||
https://medium.com/@cyb_detective/20-regular-expressions-examples-to-search-for-data-related-to-cryptocurrencies-43e31dd4a5dc
|
||||
"""
|
||||
|
||||
|
||||
class CryptoAddressExtractor:
|
||||
# A string that is treated as the name of this resolution.
|
||||
name = "Extract Cryptocurrency Addresses"
|
||||
|
||||
category = "Website Information"
|
||||
|
||||
# A string that describes this resolution.
|
||||
description = "Returns patterns matching common cryptocurrency address formats on a website."
|
||||
|
||||
originTypes = {'Domain', 'Website'}
|
||||
|
||||
resultTypes = {'Crypto Wallet'}
|
||||
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
from playwright.sync_api import sync_playwright, TimeoutError, Error
|
||||
from bs4 import BeautifulSoup
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
playwrightPath = Path(parameters['Playwright Chromium'])
|
||||
returnResults = []
|
||||
|
||||
ethRegex = re.compile(r"\b0[xX][a-fA-F0-9]{40}\b")
|
||||
btcRegex = re.compile(
|
||||
r"\b(?:bc(?:0(?:[ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59})|1[ac-hj-np-z02-9]{8,87})|[13][a-km-zA-HJ-NP-Z1-9]{25,34})\b")
|
||||
bchRegex = re.compile(r"\b(?:(?:bitcoincash|bchreg|bchtest):)?[qp][a-z0-9]{41}\b")
|
||||
moneroRegex = re.compile(r"\b[48][0-9AB][1-9A-HJ-NP-Za-km-z]{93}\b")
|
||||
dogeRegex = re.compile(r"\bD[5-9A-HJ-NP-U][1-9A-HJ-NP-Za-km-z]{32}\b")
|
||||
dashRegex = re.compile(r"\bX[1-9A-HJ-NP-Za-km-z]{33}\b")
|
||||
rippleRegex = re.compile(r"\br[1-9A-HJ-NP-Za-km-z]{24,34}\b")
|
||||
neoRegex = re.compile(r"\bN[0-9a-zA-Z]{33}\b")
|
||||
litecoinRegex = re.compile(r"\b[LM3][a-km-zA-HJ-NP-Z1-9]{26,33}\b")
|
||||
cosmosRegex = re.compile(r"\bcosmos[a-zA-Z0-9_.-]{10,}\b")
|
||||
cardanoRegex = re.compile(r"\baddr1[a-z0-9]{10,}\b")
|
||||
iotaRegex = re.compile(r"\biota[a-z0-9]{10,}\b")
|
||||
liskRegex = re.compile(r"\b[0-9]{19}L\b")
|
||||
nemRegex = re.compile(
|
||||
r"\bN[A-Za-z0-9]{4,7}-[A-Za-z0-9]{4,7}-[A-Za-z0-9]{4,7}-[A-Za-z0-9]{4,7}-[A-Za-z0-9]{4,7}-[A-Za-z0-9]{4,7}-[A-Za-z0-9]{4,7}\b")
|
||||
ontologyRegex = re.compile(r"\bA[0-9a-zA-Z]{33}\b")
|
||||
polkadotRegex = re.compile(r"\b1[0-9a-zA-Z]{47}\b")
|
||||
stellarRegex = re.compile(r"\bG[0-9A-Z]{55}\b") # Stellar addresses are always 56 characters long.
|
||||
|
||||
# The software can deduplicate, but handling it here is better.
|
||||
allWallets = set()
|
||||
|
||||
def extractCryptoAddresses(currentUID: str, site: str):
|
||||
page = context.new_page()
|
||||
pageResolved = False
|
||||
for _ in range(3):
|
||||
try:
|
||||
page.goto(site, wait_until="networkidle", timeout=10000)
|
||||
pageResolved = True
|
||||
break
|
||||
except TimeoutError:
|
||||
pass
|
||||
except Error:
|
||||
break
|
||||
if not pageResolved:
|
||||
# Last chance for this to work; some pages have issues with the "networkidle" trigger.
|
||||
try:
|
||||
page.goto(site, wait_until="load", timeout=10000)
|
||||
except Error:
|
||||
return
|
||||
|
||||
soupContents = BeautifulSoup(page.content(), 'lxml')
|
||||
# Remove <span> and <noscript> tags.
|
||||
while True:
|
||||
try:
|
||||
soupContents.noscript.extract()
|
||||
except AttributeError:
|
||||
break
|
||||
while True:
|
||||
try:
|
||||
soupContents.span.extract()
|
||||
except AttributeError:
|
||||
break
|
||||
siteContent = soupContents.get_text()
|
||||
|
||||
ethMatch = ethRegex.findall(siteContent)
|
||||
for potentialMatch in ethMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Etherium',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Etherium Wallet Address',
|
||||
'Notes': ''}}])
|
||||
btcMatch = btcRegex.findall(siteContent)
|
||||
for potentialMatch in btcMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Bitcoin',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Bitcoin or Bitcoin Cash Wallet Address',
|
||||
'Notes': ''}}])
|
||||
bchMatch = bchRegex.findall(siteContent)
|
||||
for potentialMatch in bchMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Bitcoin Cash',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Bitcoin Cash Wallet Address',
|
||||
'Notes': ''}}])
|
||||
xmrMatch = moneroRegex.findall(siteContent)
|
||||
for potentialMatch in xmrMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Monero',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Monero Wallet Address',
|
||||
'Notes': ''}}])
|
||||
dogeMatch = dogeRegex.findall(siteContent)
|
||||
for potentialMatch in dogeMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Dogecoin',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Dogecoin Wallet Address',
|
||||
'Notes': ''}}])
|
||||
dashMatch = dashRegex.findall(siteContent)
|
||||
for potentialMatch in dashMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Dash',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Dash Wallet Address',
|
||||
'Notes': ''}}])
|
||||
rippleMatch = rippleRegex.findall(siteContent)
|
||||
for potentialMatch in rippleMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Ripple',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Ripple Wallet Address',
|
||||
'Notes': ''}}])
|
||||
neoMatch = neoRegex.findall(siteContent)
|
||||
for potentialMatch in neoMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Neo',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Neo Wallet Address',
|
||||
'Notes': ''}}])
|
||||
litecoinMatch = litecoinRegex.findall(siteContent)
|
||||
for potentialMatch in litecoinMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Litecoin',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Litecoin Wallet Address',
|
||||
'Notes': ''}}])
|
||||
cosmosMatch = cosmosRegex.findall(siteContent)
|
||||
for potentialMatch in cosmosMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Cosmos',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Cosmos Wallet Address',
|
||||
'Notes': ''}}])
|
||||
cardanoMatch = cardanoRegex.findall(siteContent)
|
||||
for potentialMatch in cardanoMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Cardano',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Cardano Wallet Address',
|
||||
'Notes': ''}}])
|
||||
iotaMatch = iotaRegex.findall(siteContent)
|
||||
for potentialMatch in iotaMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Iota',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Iota Wallet Address',
|
||||
'Notes': ''}}])
|
||||
liskMatch = liskRegex.findall(siteContent)
|
||||
for potentialMatch in liskMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Lisk',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Lisk Wallet Address',
|
||||
'Notes': ''}}])
|
||||
nemMatch = nemRegex.findall(siteContent)
|
||||
for potentialMatch in nemMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Nem',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Nem Wallet Address',
|
||||
'Notes': ''}}])
|
||||
ontologyMatch = ontologyRegex.findall(siteContent)
|
||||
for potentialMatch in ontologyMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Ontology',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Ontology Wallet Address',
|
||||
'Notes': ''}}])
|
||||
polkadotMatch = polkadotRegex.findall(siteContent)
|
||||
for potentialMatch in polkadotMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Polkadot',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Polkadot Wallet Address',
|
||||
'Notes': ''}}])
|
||||
stellarMatch = stellarRegex.findall(siteContent)
|
||||
for potentialMatch in stellarMatch:
|
||||
if potentialMatch not in allWallets:
|
||||
allWallets.add(potentialMatch)
|
||||
returnResults.append([{'Wallet Address': potentialMatch,
|
||||
'Currency Name': 'Stellar',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{currentUID: {'Resolution': 'Potential Stellar Wallet Address',
|
||||
'Notes': ''}}])
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(executable_path=playwrightPath)
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
'Chrome/101.0.4951.54 Safari/537.36'
|
||||
)
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
url = entity.get('URL') if entity.get('Entity Type', '') == 'Website' else \
|
||||
entity.get('Domain Name', None)
|
||||
if url is None:
|
||||
continue
|
||||
if not url.startswith('http://') and not url.startswith('https://'):
|
||||
url = f'http://{url}'
|
||||
extractCryptoAddresses(uid, url)
|
||||
browser.close()
|
||||
|
||||
return returnResults
|
||||
@@ -47,11 +47,10 @@ class DecodePhrase:
|
||||
if len(text) % 8 != 0:
|
||||
return "Malformed format not in Octaves"
|
||||
|
||||
ascii_string = ''
|
||||
for binaryIndex in range(0, len(text), 8):
|
||||
ascii_string += chr(int(text[binaryIndex:binaryIndex + 8], 2))
|
||||
returnResult.append([{'Phrase': str(ascii_string),
|
||||
'Entity Type': 'Phrase'},
|
||||
ascii_string = ''.join(chr(int(text[binaryIndex: binaryIndex + 8], 2))
|
||||
for binaryIndex in range(0, len(text), 8))
|
||||
|
||||
returnResult.append([{'Phrase': ascii_string, 'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': 'Binary Decoded Phrase', 'Notes': ''}}])
|
||||
|
||||
return returnResult
|
||||
|
||||
51
Core/Resolutions/Core/DecodeRedirectUrlParameter.py
Normal file
51
Core/Resolutions/Core/DecodeRedirectUrlParameter.py
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class DecodeRedirectUrlParameter:
|
||||
name = "Decode Redirect URL"
|
||||
category = "Website Information"
|
||||
description = "Search the URL's parameters to see where you'll be redirected."
|
||||
originTypes = {'Website'}
|
||||
resultTypes = {'Website'}
|
||||
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import contextlib
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import parse_qs
|
||||
from urllib.parse import unquote
|
||||
from base64 import b64decode
|
||||
|
||||
def get_redirect_value(parameter_arg: str) -> str:
|
||||
with contextlib.suppress(Exception):
|
||||
clean_val = unquote(parameter_arg)
|
||||
if urlparse(clean_val).scheme:
|
||||
return clean_val
|
||||
clean_val = unquote(b64decode(parameter_arg).decode('UTF-8'))
|
||||
if urlparse(clean_val).scheme:
|
||||
return clean_val
|
||||
return ''
|
||||
|
||||
returnResults = []
|
||||
|
||||
for entity in entityJsonList:
|
||||
primaryField = entity['URL'].strip()
|
||||
parsed_url = urlparse(primaryField, allow_fragments=False)
|
||||
|
||||
parsed_url_params = parse_qs(parsed_url.query)
|
||||
for param, param_value in parsed_url_params.items():
|
||||
param_potential_url_value = get_redirect_value(', '.join(param_value))
|
||||
if param_potential_url_value:
|
||||
parsed_url_params_copy = dict(parsed_url_params)
|
||||
parsed_url_params_copy.pop(param)
|
||||
new_entity = {'URL': param_potential_url_value,
|
||||
'Entity Type': 'Website'}
|
||||
for param_copy, param_value_copy in parsed_url_params_copy.items():
|
||||
new_entity[param_copy] = ', '.join(param_value_copy)
|
||||
returnResults.append([new_entity,
|
||||
{entity['uid']: {'Resolution': 'Redirect To',
|
||||
'Notes': ''}}])
|
||||
break
|
||||
|
||||
return returnResults
|
||||
76
Core/Resolutions/Core/DeleteColumn.py
Normal file
76
Core/Resolutions/Core/DeleteColumn.py
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class DeleteColumn:
|
||||
# A string that is treated as the name of this resolution.
|
||||
name = "Rename or Delete Column"
|
||||
|
||||
category = "Spreadsheet Operations"
|
||||
|
||||
# A string that describes this resolution.
|
||||
description = "Deletes the column with the specified index from a Spreadsheet document."
|
||||
|
||||
originTypes = {'Spreadsheet'}
|
||||
|
||||
resultTypes = {'Spreadsheet'}
|
||||
|
||||
parameters = {'Working Sheet': {'description': 'The name or index of the Sheet to read in the Spreadsheet '
|
||||
'file. By default, the first Sheet is used.',
|
||||
'type': 'String',
|
||||
'value': '0',
|
||||
'default': '0'},
|
||||
'Column Name to Rename': {'description': 'Please enter the name of the column that you wish to '
|
||||
'rename or delete.',
|
||||
'type': 'String',
|
||||
'value': ''},
|
||||
'New Column Name': {'description': 'Please enter the new name for the column.\nEnter the same name '
|
||||
'to delete the column instead.',
|
||||
'type': 'String',
|
||||
'value': ''}
|
||||
}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
from pathlib import Path
|
||||
import pandas as pd
|
||||
import contextlib
|
||||
|
||||
workingSheet = parameters['Working Sheet']
|
||||
with contextlib.suppress(ValueError):
|
||||
workingSheet = int(workingSheet)
|
||||
|
||||
renameColumn = parameters['Column Name to Rename']
|
||||
targetColumn = parameters['New Column Name']
|
||||
|
||||
returnResults = []
|
||||
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
filePath = Path(parameters['Project Files Directory']) / entity['File Path']
|
||||
if not filePath.exists() or not filePath.is_file():
|
||||
continue
|
||||
|
||||
try:
|
||||
csvDF = pd.read_excel(filePath, sheet_name=workingSheet)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if renameColumn == targetColumn:
|
||||
csvDF.drop(renameColumn, inplace=True)
|
||||
else:
|
||||
csvDF.rename(columns={renameColumn: targetColumn}, inplace=True)
|
||||
|
||||
count = 0
|
||||
while True:
|
||||
newFileName = f"{filePath.name.split(filePath.suffix, 1)[0]}-c{count}{filePath.suffix}"
|
||||
newFilePath = filePath.parent / newFileName
|
||||
if not newFilePath.exists():
|
||||
break
|
||||
csvDF.to_excel(newFilePath, index=False)
|
||||
|
||||
returnResults.append([{'Spreadsheet Name': newFileName,
|
||||
'File Path': newFileName,
|
||||
'Entity Type': 'Spreadsheet'},
|
||||
{uid: {'Resolution': 'Rename/Delete Column',
|
||||
'Notes': ''}}])
|
||||
|
||||
return returnResults
|
||||
@@ -12,6 +12,7 @@ class DomainFromPhrase:
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import re
|
||||
import contextlib
|
||||
import tldextract
|
||||
|
||||
domainRegex = re.compile(
|
||||
@@ -30,14 +31,11 @@ class DomainFromPhrase:
|
||||
while wordChar.match(entityChunk[-1]) is None:
|
||||
entityChunk = entityChunk[:-1]
|
||||
if domainRegex.match(entityChunk):
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
tldObject = tldextract.extract(entityChunk)
|
||||
if tldObject.suffix != '':
|
||||
returnResults.append([{'Domain Name': entityChunk,
|
||||
'Entity Type': 'Domain'},
|
||||
{entity['uid']: {'Resolution': 'Phrase To Domain',
|
||||
'Notes': ''}}])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return returnResults
|
||||
|
||||
@@ -37,18 +37,22 @@ class EmailExtractor:
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
from playwright.sync_api import sync_playwright, TimeoutError, Error
|
||||
from bs4 import BeautifulSoup
|
||||
from pathlib import Path
|
||||
import re
|
||||
import contextlib
|
||||
from email_validator import validate_email, caching_resolver, EmailNotValidError
|
||||
|
||||
playwrightPath = Path(parameters['Playwright Chromium'])
|
||||
returnResults = []
|
||||
|
||||
# Source: https://emailregex.com/
|
||||
# Alt: (?:[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(\.([a-zA-Z0-9-])+)+)
|
||||
emailRegex = re.compile(r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""")
|
||||
useRegex = True if parameters['Use Regex'] == 'Yes' else False
|
||||
emailRegex = re.compile(
|
||||
r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""")
|
||||
useRegex = parameters['Use Regex'] == 'Yes'
|
||||
|
||||
resolver = caching_resolver(timeout=10)
|
||||
verifyDomain = True if parameters['Verify Email Domain Validity'] == 'Yes' else False
|
||||
verifyDomain = parameters['Verify Email Domain Validity'] == 'Yes'
|
||||
|
||||
# The software can deduplicate, but handling it here is better.
|
||||
allEmails = set()
|
||||
@@ -89,42 +93,38 @@ class EmailExtractor:
|
||||
siteContent = re.sub(r'\s*(\[|\<|\()+\s*at\s*(\]|\>|\))+\s*', '@', siteContent)
|
||||
siteContent = re.sub(r'\s*(\[|\<|\()+\s*dot\s*(\]|\>|\))+\s*', '.', siteContent)
|
||||
siteContent = re.sub(r'\s*(\[|\<|\()+\s*\.\s*(\]|\>|\))+\s*', '.', siteContent)
|
||||
siteContent = re.sub(r'\s*@\s*', '@', siteContent)
|
||||
siteContent = re.sub(r'\s*\.', '.', siteContent)
|
||||
siteContent = re.sub(r'\.\s*([^A-Z])', r'.\1', siteContent)
|
||||
|
||||
potentialEmails = emailRegex.findall(siteContent)
|
||||
for potentialEmail in potentialEmails:
|
||||
try:
|
||||
with contextlib.suppress(EmailNotValidError):
|
||||
valid = validate_email(potentialEmail, dns_resolver=resolver, check_deliverability=verifyDomain)
|
||||
if valid.email not in allEmails:
|
||||
allEmails.add(valid.email)
|
||||
returnResults.append([{'Email Address': valid.email,
|
||||
if valid.normalized not in allEmails:
|
||||
allEmails.add(valid.normalized)
|
||||
returnResults.append([{'Email Address': valid.normalized,
|
||||
'Entity Type': 'Email Address'},
|
||||
{currentUID: {'Resolution': 'Email Address Found',
|
||||
'Notes': ''}}])
|
||||
except EmailNotValidError:
|
||||
pass
|
||||
linksInAHref = soupContents.find_all('a')
|
||||
for tag in linksInAHref:
|
||||
newLink = tag.get('href', None)
|
||||
if newLink is not None:
|
||||
if newLink.startswith('mailto:'):
|
||||
try:
|
||||
valid = validate_email(newLink[7:], dns_resolver=resolver,
|
||||
check_deliverability=verifyDomain)
|
||||
if valid.email not in allEmails:
|
||||
allEmails.add(valid.email)
|
||||
returnResults.append([{'Email Address': valid.email,
|
||||
'Entity Type': 'Email Address'},
|
||||
{currentUID: {'Resolution': 'Email Address Found',
|
||||
'Notes': ''}}])
|
||||
except EmailNotValidError:
|
||||
pass
|
||||
if newLink is not None and newLink.startswith('mailto:'):
|
||||
with contextlib.suppress(EmailNotValidError):
|
||||
valid = validate_email(newLink[7:], dns_resolver=resolver,
|
||||
check_deliverability=verifyDomain)
|
||||
if valid.normalized not in allEmails:
|
||||
allEmails.add(valid.normalized)
|
||||
returnResults.append([{'Email Address': valid.normalized,
|
||||
'Entity Type': 'Email Address'},
|
||||
{currentUID: {'Resolution': 'Email Address Found',
|
||||
'Notes': ''}}])
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
browser = p.chromium.launch(executable_path=playwrightPath)
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
'Chrome/101.0.4951.54 Safari/537.36'
|
||||
viewport={'width': 1920, 'height': 1080}
|
||||
)
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
@@ -133,7 +133,7 @@ class EmailExtractor:
|
||||
if url is None:
|
||||
continue
|
||||
if not url.startswith('http://') and not url.startswith('https://'):
|
||||
url = 'http://' + url
|
||||
url = f'http://{url}'
|
||||
extractEmails(uid, url)
|
||||
browser.close()
|
||||
|
||||
|
||||
@@ -11,18 +11,16 @@ class EmailToDomain:
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import contextlib
|
||||
|
||||
returnResults = []
|
||||
|
||||
for entity in entityJsonList:
|
||||
primaryField = entity['Email Address']
|
||||
# There is no provider that I am aware of that allows '@' signs in the user part of the email.
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
returnResults.append([{'Domain Name': primaryField.split('@')[1].strip(),
|
||||
'Entity Type': 'Domain'},
|
||||
{entity['uid']: {'Resolution': 'Email To Domain',
|
||||
'Notes': ''}}])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return returnResults
|
||||
|
||||
@@ -26,7 +26,7 @@ class ExtractDOCXMeta:
|
||||
uid = entity['uid']
|
||||
filePath = Path(parameters['Project Files Directory']) / entity['File Path']
|
||||
|
||||
if not (filePath.exists() and filePath.is_file()):
|
||||
if not filePath.exists() or not filePath.is_file():
|
||||
continue
|
||||
|
||||
if magic.from_file(str(filePath), mime=True) != \
|
||||
@@ -49,9 +49,8 @@ class ExtractDOCXMeta:
|
||||
'Entity Type': 'Date'},
|
||||
{uid: {'Resolution': 'created', 'Notes': ''}}])
|
||||
|
||||
for metadataKey in [dataKey for dataKey in data if dataKey not in defaultDateProperties]:
|
||||
returnResults.append([{'Phrase': metadataKey + ': ' + str(data.get(metadataKey)),
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': metadataKey, 'Notes': ''}}])
|
||||
returnResults.extend([{'Phrase': f'{metadataKey}: {str(data.get(metadataKey))}', 'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': metadataKey, 'Notes': ''}}] for metadataKey in
|
||||
[dataKey for dataKey in data if dataKey not in defaultDateProperties])
|
||||
|
||||
return returnResults
|
||||
|
||||
@@ -7,7 +7,7 @@ class ExtractPDFMeta:
|
||||
category = "File Operations"
|
||||
|
||||
# A string that describes this resolution.
|
||||
description = "Returns a set of nodes that contain all the metadata info of pdf files."
|
||||
description = "Returns a set of nodes that contain notable metadata info of pdf files."
|
||||
|
||||
originTypes = {'Document'}
|
||||
|
||||
@@ -16,7 +16,8 @@ class ExtractPDFMeta:
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
from PyPDF2 import PdfFileReader
|
||||
from pypdf import PdfReader
|
||||
from datetime import datetime, timedelta
|
||||
import magic
|
||||
from pathlib import Path
|
||||
|
||||
@@ -26,44 +27,55 @@ class ExtractPDFMeta:
|
||||
uid = entity['uid']
|
||||
filePath = Path(parameters['Project Files Directory']) / entity['File Path']
|
||||
|
||||
if not (filePath.exists() and filePath.is_file()):
|
||||
if not (filePath.is_file()):
|
||||
continue
|
||||
|
||||
if magic.from_file(str(filePath), mime=True) != \
|
||||
'application/pdf':
|
||||
if magic.from_file(str(filePath), mime=True) != 'application/pdf':
|
||||
continue
|
||||
|
||||
with open(filePath, 'rb') as f:
|
||||
pdf = PdfFileReader(f)
|
||||
info = pdf.getDocumentInfo()
|
||||
number_of_pages = pdf.getNumPages()
|
||||
pdf = PdfReader(f)
|
||||
info = pdf.metadata
|
||||
number_of_pages = len(pdf.pages)
|
||||
|
||||
for metadataKey in info:
|
||||
attrValue = metadataKey[1:] if metadataKey.startswith('/') else metadataKey
|
||||
if 'Date' in metadataKey:
|
||||
try:
|
||||
strDate = info[metadataKey]
|
||||
strDate = strDate.split(':')[1].split('-')[0]
|
||||
strDate1 = strDate[:-6]
|
||||
strDate2 = strDate[-6:]
|
||||
strDate2 = ':'.join(strDate2[i:i+2] for i in range(0, 6, 2))
|
||||
strDate1 = strDate1[:-4] + '-' + '-'.join(strDate1[::-1][i:i+2] for i in range(0, 4, 2))[::-1]
|
||||
strDate = strDate1 + 'T' + strDate2
|
||||
returnResults.append([{'Date': strDate,
|
||||
strDate = info[metadataKey].split(':', 1)[1]
|
||||
if strDate.endswith('Z'):
|
||||
dateString = datetime.strptime(strDate, "%Y%m%d%H%M%SZ").isoformat()
|
||||
elif '+' in strDate:
|
||||
datePart1, datePart2 = strDate.split('+', 1)
|
||||
date1 = datetime.strptime(datePart1, "%Y%m%d%H%M%S")
|
||||
date2 = timedelta(hours=int(datePart2.split("'")[0]), minutes=int(datePart2.split("'")[1]))
|
||||
dateString = (date1 + date2).isoformat()
|
||||
elif '-' in strDate:
|
||||
datePart1, datePart2 = strDate.split('-', 1)
|
||||
date1 = datetime.strptime(datePart1, "%Y%m%d%H%M%S")
|
||||
date2 = timedelta(hours=int(datePart2.split("'")[0]), minutes=int(datePart2.split("'")[1]))
|
||||
dateString = (date1 - date2).isoformat()
|
||||
else:
|
||||
raise ValueError('Cannot parse Date format.')
|
||||
|
||||
returnResults.append([{'Date': dateString,
|
||||
'Entity Type': 'Date'},
|
||||
{uid: {'Resolution': metadataKey, 'Notes': ''}}])
|
||||
{uid: {'Resolution': attrValue, 'Notes': ''}}])
|
||||
except Exception:
|
||||
# Reset strDate to default value
|
||||
strDate = info[metadataKey]
|
||||
returnResults.append([{'Date': strDate,
|
||||
'Entity Type': 'Date'},
|
||||
{uid: {'Resolution': metadataKey, 'Notes': ''}}])
|
||||
{uid: {'Resolution': attrValue, 'Notes': ''}}])
|
||||
else:
|
||||
returnResults.append([{'Phrase': metadataKey + ': ' + str(info[metadataKey]),
|
||||
# Clean some misshapen strings
|
||||
value = str(info[metadataKey])
|
||||
value = value.removeprefix('/')
|
||||
returnResults.append([{'Phrase': f'{attrValue}: {value}',
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': metadataKey, 'Notes': ''}}])
|
||||
{uid: {'Resolution': attrValue, 'Notes': ''}}])
|
||||
|
||||
returnResults.append([{'Phrase': 'Number of Pages: ' + str(number_of_pages),
|
||||
'Entity Type': 'Phrase'},
|
||||
returnResults.append([{'Phrase': f'Number of Pages: {number_of_pages}', 'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': 'Number of Pages', 'Notes': ''}}])
|
||||
|
||||
return returnResults
|
||||
|
||||
@@ -26,7 +26,7 @@ class FileExtractor:
|
||||
|
||||
originTypes = {'Domain', 'Website'}
|
||||
|
||||
resultTypes = {'Website', 'Document', 'Image', 'Video', 'Archive'}
|
||||
resultTypes = {'Website', 'Document', 'Spreadsheet', 'Image', 'Video', 'Archive'}
|
||||
|
||||
parameters = {'Max Depth': {'description': 'Each link leading to another website in the same domain can be '
|
||||
'explored to discover more entities. Each entity discovered after '
|
||||
@@ -41,27 +41,30 @@ class FileExtractor:
|
||||
'default': '0'}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import contextlib
|
||||
import tldextract
|
||||
import requests
|
||||
from hashlib import md5
|
||||
from binascii import hexlify
|
||||
from pathlib import Path
|
||||
from bs4 import BeautifulSoup
|
||||
from playwright.sync_api import sync_playwright, TimeoutError, Error
|
||||
|
||||
playwrightPath = Path(parameters['Playwright Firefox'])
|
||||
|
||||
try:
|
||||
maxDepth = max(int(parameters['Max Depth']), 0)
|
||||
except ValueError:
|
||||
return "Invalid value provided for Max Webpages to follow."
|
||||
|
||||
fileTypes = (".sxw", ".odt", ".ods", ".odg", ".odp", ".docx", ".xlsx", ".pptx", ".ppsx", ".doc", ".xls",
|
||||
fileTypes = (".sxw", ".odt", ".odg", ".odp", ".docx", ".pptx", ".ppsx", ".doc", ".csv",
|
||||
".ppt", ".pps", ".pdf", ".wpd", ".raw", ".cr2", ".crw", ".indd", ".rdp", ".ica", ".ico", ".txt",
|
||||
".text", ".bak", ".log", ".env", ".pub", ".docm", ".xlsm", ".old", ".csv", ".apk", ".sql", ".cfg",
|
||||
".text", ".bak", ".log", ".env", ".pub", ".docm", ".old", ".apk", ".sql", ".cfg",
|
||||
".key", ".reg", ".yml", ".yaml", ".mail", ".eml", ".mbox", ".mbx", ".url", ".csr", ".config",
|
||||
".mdb", ".user", ".adr", ".ini", ".plist", ".conf", ".dat", ".pcf", ".bok", ".properties", ".json",
|
||||
".backup", ".sh", ".py", ".md", ".inc")
|
||||
videoTypes = (".mp3", ".mp4")
|
||||
imageTypes = (".jpg", ".jpeg", ".png", ".svg", ".svgz")
|
||||
".backup", ".sh", ".py", ".md", ".inc", '.ovpn', '.bat')
|
||||
spreadsheetTypes = (".xlsx", ".xls", ".ods", ".xlsm")
|
||||
videoTypes = (".mp3", ".mp4", ".mov", ".webm", ".amv")
|
||||
imageTypes = (".jpg", ".jpeg", ".png", ".svg", ".svgz", ".bmp")
|
||||
archiveTypes = (".zip", ".rar", ".7z", ".gz")
|
||||
|
||||
returnResults = []
|
||||
@@ -87,7 +90,7 @@ class FileExtractor:
|
||||
if link is not None:
|
||||
if not link.startswith('http'):
|
||||
# We assume that we will be redirected to https if available.
|
||||
link = 'http://' + domain + link
|
||||
link = f'http://{domain}{link}'
|
||||
link = link.split('#')[0]
|
||||
if link not in urlsExplored:
|
||||
urlsExplored.add(link)
|
||||
@@ -103,6 +106,8 @@ class FileExtractor:
|
||||
fileTypeIdentified = 'Image'
|
||||
elif link.endswith(archiveTypes):
|
||||
fileTypeIdentified = 'Archive'
|
||||
elif link.endswith(spreadsheetTypes):
|
||||
fileTypeIdentified = 'Spreadsheet'
|
||||
|
||||
if fileTypeIdentified:
|
||||
childIndex = len(returnResults)
|
||||
@@ -113,10 +118,10 @@ class FileExtractor:
|
||||
'Notes': ''}}])
|
||||
|
||||
docProperName = link.split('/')[-1]
|
||||
docFileName = hexlify(md5(link.encode()).digest()).decode() + ' | ' + docProperName # nosec
|
||||
docFileName = f'{md5(link.encode("UTF-8")).hexdigest()}_{docProperName}'
|
||||
docFullPath = Path(parameters['Project Files Directory']) / docFileName
|
||||
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
response = requests.get(link,
|
||||
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; '
|
||||
'x64; rv:94.0) Gecko/20100101 '
|
||||
@@ -126,13 +131,10 @@ class FileExtractor:
|
||||
for chunk in response.iter_content(4096):
|
||||
fileToWrite.write(chunk)
|
||||
|
||||
returnResults.append([{fileTypeIdentified + ' Name': docProperName,
|
||||
returnResults.append([{f'{fileTypeIdentified} Name': docProperName,
|
||||
'File Path': docFileName,
|
||||
'Entity Type': fileTypeIdentified},
|
||||
{childIndex: {'Resolution': 'Downloaded File',
|
||||
'Notes': ''}}])
|
||||
except Exception:
|
||||
pass
|
||||
{childIndex: {'Resolution': 'Downloaded File', 'Notes': ''}}])
|
||||
|
||||
elif domain in link:
|
||||
urlsToExplore.add(link)
|
||||
@@ -143,7 +145,7 @@ class FileExtractor:
|
||||
if link is not None:
|
||||
if not link.startswith('http'):
|
||||
# We assume that we will be redirected to https if available.
|
||||
link = 'http://' + domain + link
|
||||
link = f'http://{domain}{link}'
|
||||
link = link.split('#')[0]
|
||||
if link not in urlsExplored:
|
||||
urlsExplored.add(link)
|
||||
@@ -155,10 +157,10 @@ class FileExtractor:
|
||||
{uid: {'Resolution': 'File URL',
|
||||
'Notes': ''}}])
|
||||
docProperName = link.split('/')[-1]
|
||||
docFileName = hexlify(md5(link.encode()).digest()).decode() + ' | ' + docProperName # nosec
|
||||
docFileName = f'{md5(link.encode()).hexdigest()}_{docProperName}'
|
||||
docFullPath = Path(parameters['Project Files Directory']) / docFileName
|
||||
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
response = requests.get(link,
|
||||
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; '
|
||||
'x64; rv:94.0) Gecko/20100101 '
|
||||
@@ -173,16 +175,13 @@ class FileExtractor:
|
||||
'Entity Type': 'Image'},
|
||||
{childIndex: {'Resolution': 'Downloaded File',
|
||||
'Notes': ''}}])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if currentDepth > 0:
|
||||
newDepth = currentDepth - 1
|
||||
for newURL in urlsToExplore:
|
||||
iterateOnDepth(newURL, newDepth)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.firefox.launch()
|
||||
browser = p.firefox.launch(executable_path=playwrightPath)
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0'
|
||||
@@ -194,7 +193,7 @@ class FileExtractor:
|
||||
if url is None:
|
||||
continue
|
||||
if not url.startswith('http://') and not url.startswith('https://'):
|
||||
url = 'http://' + url
|
||||
url = f'http://{url}'
|
||||
domain = tldextract.extract(url).fqdn
|
||||
|
||||
# Because these do not persist across entities, it is possible to explore a URL multiple times.
|
||||
|
||||
@@ -5,30 +5,30 @@ class FileHasher:
|
||||
name = "Get File Hash"
|
||||
category = "File Operations"
|
||||
description = "Get the Hash of a file."
|
||||
originTypes = {"Image", "Document", "Video", "Archive", "Disk"}
|
||||
originTypes = {"Image", "Document", "Spreadsheet", "Video", "Archive", "Disk"}
|
||||
resultTypes = {'Hash'}
|
||||
parameters = {'hashing_algorithms': {'description': 'The type of hash/es that will be returned',
|
||||
'type': 'MultiChoice',
|
||||
'value': {'SHA1', 'SHA256', 'MD5'}
|
||||
}}
|
||||
parameters = {'Hashing Algorithm': {'description': 'Choose the type of hash(es) that you want to be returned:',
|
||||
'type': 'MultiChoice',
|
||||
'value': {'SHA1', 'SHA256', 'MD5'}
|
||||
}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
return_result = []
|
||||
hashing_algorithms = parameters['hashing_algorithms']
|
||||
hashing_algorithms = parameters['Hashing Algorithm']
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
file_path = Path(parameters['Project Files Directory']) / entity['File Path']
|
||||
if not (file_path.exists() and file_path.is_file()):
|
||||
if not file_path.is_file():
|
||||
continue
|
||||
block_size = 65536 # The size of each read from the file
|
||||
for hashing_algorithm in hashing_algorithms:
|
||||
if hashing_algorithm == "SHA256":
|
||||
file_hash = hashlib.sha256() # nosec
|
||||
elif hashing_algorithm == "SHA1":
|
||||
if hashing_algorithm == "SHA1":
|
||||
file_hash = hashlib.sha1() # nosec
|
||||
elif hashing_algorithm == "SHA256":
|
||||
file_hash = hashlib.sha256() # nosec
|
||||
else:
|
||||
file_hash = hashlib.md5() # nosec
|
||||
with open(file_path, 'rb') as f:
|
||||
@@ -40,5 +40,5 @@ class FileHasher:
|
||||
return_result.append([{'Hash Value': resulting_hash,
|
||||
'Hash Algorithm': hashing_algorithm,
|
||||
'Entity Type': 'Hash'},
|
||||
{uid: {'Resolution': hashing_algorithm + ' Hash', 'Notes': ''}}])
|
||||
{uid: {'Resolution': f'{hashing_algorithm} Hash', 'Notes': ''}}])
|
||||
return return_result
|
||||
|
||||
@@ -39,8 +39,14 @@ class GetExternalURLs:
|
||||
from playwright.sync_api import sync_playwright, TimeoutError, Error
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
import urllib.parse
|
||||
import contextlib
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import parse_qs
|
||||
from urllib.parse import unquote
|
||||
from base64 import b64decode
|
||||
from pathlib import Path
|
||||
|
||||
playwrightPath = Path(parameters['Playwright Chromium'])
|
||||
onionRegex = re.compile(r"""^https?://\w{56}\.onion/?(\S(?<!\.))*(\.(\S(?<!\.))*)?$""")
|
||||
returnResult = []
|
||||
|
||||
@@ -48,16 +54,23 @@ class GetExternalURLs:
|
||||
extract_img = '<img> elements' in parameters['Element types to check']
|
||||
extract_link = '<link> elements' in parameters['Element types to check']
|
||||
|
||||
# Sites like youtube replace external links with a redirect link originating
|
||||
# from the site itself. This sort of gets around that.
|
||||
redirectRegex = re.compile(r'\?.*(q|url)=\S[^&#?]+', re.IGNORECASE)
|
||||
def get_potential_redirect_value(potential_redirect_url: str) -> str:
|
||||
parsed_url = urlparse(potential_redirect_url, allow_fragments=False)
|
||||
parsed_url_params = parse_qs(parsed_url.query)
|
||||
for param, param_value in parsed_url_params.items():
|
||||
with contextlib.suppress(Exception):
|
||||
clean_val = unquote(', '.join(param_value))
|
||||
if urlparse(clean_val).scheme:
|
||||
return clean_val
|
||||
clean_val = unquote(b64decode(', '.join(param_value)).decode('UTF-8'))
|
||||
if urlparse(clean_val).scheme:
|
||||
return clean_val
|
||||
return ''
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
browser = p.chromium.launch(executable_path=playwrightPath)
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
'Chrome/101.0.4951.64 Safari/537.36'
|
||||
viewport={'width': 1920, 'height': 1080}
|
||||
)
|
||||
page = context.new_page()
|
||||
externalUrls = {}
|
||||
@@ -65,7 +78,7 @@ class GetExternalURLs:
|
||||
for site in entityJsonList:
|
||||
uid = site['uid']
|
||||
url = site['URL']
|
||||
parsedURL = urllib.parse.urlparse(url)
|
||||
parsedURL = urlparse(url)
|
||||
if not all([parsedURL.scheme, parsedURL.netloc]):
|
||||
continue
|
||||
domain = tldextract.extract(url).fqdn
|
||||
@@ -75,62 +88,57 @@ class GetExternalURLs:
|
||||
for _ in range(3):
|
||||
try:
|
||||
page.goto(url, wait_until="networkidle", timeout=10000)
|
||||
|
||||
### Youtube
|
||||
with contextlib.suppress(Exception):
|
||||
page.get_by_role("button",
|
||||
name="Reject the use of cookies and other data for the purposes described").click()
|
||||
with contextlib.suppress(Exception):
|
||||
page.get_by_role("button", name="Show more").click()
|
||||
soupContents = BeautifulSoup(page.content(), 'lxml')
|
||||
|
||||
if extract_a:
|
||||
linksInAHref = soupContents.find_all('a')
|
||||
for tag in linksInAHref:
|
||||
link = tag.get('href', None)
|
||||
parsedURL = urllib.parse.urlparse(link)
|
||||
parsedURL = urlparse(link)
|
||||
if all([parsedURL.scheme, parsedURL.netloc]):
|
||||
if domain not in link:
|
||||
if domain in link:
|
||||
redirectLink = get_potential_redirect_value(link)
|
||||
if redirectLink:
|
||||
try:
|
||||
externalUrls[redirectLink].add(uid)
|
||||
except KeyError:
|
||||
externalUrls[redirectLink] = {uid}
|
||||
else:
|
||||
newLink = link.split('#')[0].split('?')[0]
|
||||
try:
|
||||
externalUrls[newLink].add(uid)
|
||||
except KeyError:
|
||||
externalUrls[newLink] = {uid}
|
||||
else:
|
||||
redirectLinks = redirectRegex.findall(link)
|
||||
if 'redirect' in link and len(redirectLinks) > 0:
|
||||
try:
|
||||
newLink = str(urllib.parse.unquote(redirectLinks[0]))[2:]
|
||||
try:
|
||||
externalUrls[newLink].add(uid)
|
||||
except KeyError:
|
||||
externalUrls[newLink] = {uid}
|
||||
except IndexError:
|
||||
try:
|
||||
externalUrls[redirectLinks[0]].add(uid)
|
||||
except KeyError:
|
||||
externalUrls[redirectLinks[0]] = {uid}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if extract_img:
|
||||
linksInImgSrc = soupContents.find_all('img')
|
||||
for tag in linksInImgSrc:
|
||||
link = tag.get('src', None)
|
||||
parsedURL = urllib.parse.urlparse(link)
|
||||
if all([parsedURL.scheme, parsedURL.netloc]):
|
||||
if domain not in link:
|
||||
newLink = link.split('#')[0].split('?')[0]
|
||||
try:
|
||||
externalUrls[newLink].add(uid)
|
||||
except KeyError:
|
||||
externalUrls[newLink] = {uid}
|
||||
parsedURL = urlparse(link)
|
||||
if all([parsedURL.scheme, parsedURL.netloc]) and domain not in link:
|
||||
newLink = link.split('#')[0].split('?')[0]
|
||||
try:
|
||||
externalUrls[newLink].add(uid)
|
||||
except KeyError:
|
||||
externalUrls[newLink] = {uid}
|
||||
|
||||
if extract_link:
|
||||
linksInLinkHref = soupContents.find_all('link')
|
||||
for tag in linksInLinkHref:
|
||||
link = tag.get('href', None)
|
||||
parsedURL = urllib.parse.urlparse(link)
|
||||
if all([parsedURL.scheme, parsedURL.netloc]):
|
||||
if domain not in link:
|
||||
newLink = link.split('#')[0].split('?')[0]
|
||||
try:
|
||||
externalUrls[newLink].add(uid)
|
||||
except KeyError:
|
||||
externalUrls[newLink] = {uid}
|
||||
parsedURL = urlparse(link)
|
||||
if all([parsedURL.scheme, parsedURL.netloc]) and domain not in link:
|
||||
newLink = link.split('#')[0].split('?')[0]
|
||||
try:
|
||||
externalUrls[newLink].add(uid)
|
||||
except KeyError:
|
||||
externalUrls[newLink] = {uid}
|
||||
break
|
||||
except TimeoutError:
|
||||
pass
|
||||
@@ -143,12 +151,11 @@ class GetExternalURLs:
|
||||
|
||||
for externalUrl in externalUrls:
|
||||
onionCheck = onionRegex.findall(externalUrl)
|
||||
if len(onionCheck) == 1:
|
||||
for urlUid in externalUrls[externalUrl]:
|
||||
for urlUid in externalUrls[externalUrl]:
|
||||
if len(onionCheck) == 1:
|
||||
returnResult.append([{'Onion URL': externalUrl, 'Entity Type': 'Onion Website'},
|
||||
{urlUid: {'Resolution': 'External Link', 'Notes': ''}}])
|
||||
else:
|
||||
for urlUid in externalUrls[externalUrl]:
|
||||
else:
|
||||
returnResult.append([{'URL': externalUrl, 'Entity Type': 'Website'},
|
||||
{urlUid: {'Resolution': 'External Link', 'Notes': ''}}])
|
||||
|
||||
|
||||
@@ -19,12 +19,14 @@ class GetInternalURLs:
|
||||
import tldextract
|
||||
from playwright.sync_api import sync_playwright, TimeoutError, Error
|
||||
from bs4 import BeautifulSoup
|
||||
from pathlib import Path
|
||||
import urllib.parse
|
||||
|
||||
playwrightPath = Path(parameters['Playwright Chromium'])
|
||||
returnResult = []
|
||||
internalUrls = {}
|
||||
|
||||
considerResources = False if parameters['Include Resources'] == 'Only consider links to pages' else True
|
||||
considerResources = parameters['Include Resources'] != 'Only consider links to pages'
|
||||
|
||||
def handleLink(currentLink, currentUrl, currentDomain):
|
||||
if currentLink is None:
|
||||
@@ -35,17 +37,19 @@ class GetInternalURLs:
|
||||
currentLink = currentUrl + currentLink[1:]
|
||||
elif currentLink.startswith('/'):
|
||||
urlParts = urllib.parse.urlparse(currentUrl)
|
||||
currentLink = urlParts.scheme + '://' + urlParts.netloc + currentLink
|
||||
currentLink = f'{urlParts.scheme}://{urlParts.netloc}{currentLink}'
|
||||
parsedCurrentURL = urllib.parse.urlparse(currentLink)
|
||||
if all([parsedCurrentURL.scheme, parsedCurrentURL.netloc]):
|
||||
if currentDomain in currentLink:
|
||||
newLink = currentLink.split('#')[0].split('?')[0]
|
||||
if newLink.endswith('/'):
|
||||
newLink = newLink[:-1]
|
||||
return newLink
|
||||
if (
|
||||
all([parsedCurrentURL.scheme, parsedCurrentURL.netloc])
|
||||
and currentDomain in currentLink
|
||||
):
|
||||
newLink = currentLink.split('#')[0].split('?')[0]
|
||||
if newLink.endswith('/'):
|
||||
newLink = newLink[:-1]
|
||||
return newLink
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
browser = p.chromium.launch(executable_path=playwrightPath)
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
|
||||
@@ -21,15 +21,15 @@ class GetWebsiteText:
|
||||
from bs4.element import Comment
|
||||
from playwright.sync_api import sync_playwright, TimeoutError, Error
|
||||
from urllib.parse import urlparse
|
||||
from pathlib import Path
|
||||
|
||||
returnResults = []
|
||||
playwrightPath = Path(parameters['Playwright Chromium'])
|
||||
|
||||
def tag_visible(element):
|
||||
if element.parent.name in ['style', 'script', 'head', 'title', 'meta', '[document]']:
|
||||
return False
|
||||
if isinstance(element, Comment):
|
||||
return False
|
||||
return True
|
||||
return not isinstance(element, Comment)
|
||||
|
||||
def text_from_html(body):
|
||||
soup = BeautifulSoup(body, 'lxml')
|
||||
@@ -38,7 +38,7 @@ class GetWebsiteText:
|
||||
return u" ".join(t.strip() for t in visible_texts if t.strip() != '')
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
browser = p.chromium.launch(executable_path=playwrightPath)
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
@@ -59,7 +59,7 @@ class GetWebsiteText:
|
||||
try:
|
||||
page.goto(url, wait_until="networkidle", timeout=10000)
|
||||
textContent = text_from_html(page.content())
|
||||
returnResults.append([{'Phrase': 'Website Body of: ' + url,
|
||||
returnResults.append([{'Phrase': f'Website Body of: {url}',
|
||||
'Notes': textContent,
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': 'Website Body', 'Notes': ''}}])
|
||||
|
||||
@@ -16,8 +16,7 @@ class HostnameToDomain:
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
primary_field = entity[list(entity)[1]].strip()
|
||||
tsd, td, tsu = extract(primary_field)
|
||||
domain = td + '.' + tsu
|
||||
domain = extract(primary_field).fqdn
|
||||
if domain == primary_field:
|
||||
continue
|
||||
return_result.append([{'Domain Name': domain,
|
||||
|
||||
@@ -35,19 +35,19 @@ class IPToASN:
|
||||
index_of_child = len(returnResult)
|
||||
countryCode = results['asn_country_code']
|
||||
country = pycountry.countries.get(alpha_2=countryCode).name
|
||||
returnResult.append([{'AS Number': "AS" + results['asn'],
|
||||
'ASN Cidr': results['asn_cidr'],
|
||||
'Date Created': results['asn_date'],
|
||||
'Entity Type': 'Autonomous System'},
|
||||
{uid: {'Resolution': 'Autonomous System of IP', 'Notes': ''}}])
|
||||
returnResult.append(
|
||||
[{'Organization Name': results['asn_registry'], 'Entity Type': 'Organization'},
|
||||
{index_of_child: {'Resolution': 'ASN Registry', 'Notes': ''}}])
|
||||
returnResult.append(
|
||||
[{'Country Name': country, 'Entity Type': 'Country'},
|
||||
{index_of_child: {'Resolution': 'Country of Registry for ASN', 'Notes': ''}}])
|
||||
returnResult.append(
|
||||
[{'Phrase': results['asn_description'], 'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'ASN Description', 'Notes': ''}}])
|
||||
returnResult.extend(([{'AS Number': "AS" + results['asn'],
|
||||
'ASN Cidr': results['asn_cidr'],
|
||||
'Date Created': results['asn_date'],
|
||||
'Entity Type': 'Autonomous System'},
|
||||
{uid: {'Resolution': 'Autonomous System of IP', 'Notes': ''}}],
|
||||
[{'Organization Name': results['asn_registry'],
|
||||
'Entity Type': 'Organization'},
|
||||
{index_of_child: {'Resolution': 'ASN Registry', 'Notes': ''}}],
|
||||
[{'Country Name': country,
|
||||
'Entity Type': 'Country'},
|
||||
{index_of_child: {'Resolution': 'Country of Registry for ASN', 'Notes': ''}}],
|
||||
[{'Phrase': results['asn_description'],
|
||||
'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'ASN Description', 'Notes': ''}}]))
|
||||
|
||||
return returnResult
|
||||
|
||||
@@ -31,8 +31,7 @@ class IPWhois:
|
||||
{uid: {'Resolution': 'IPWhois', 'Notes': ''}}])
|
||||
for net in response['nets']:
|
||||
if net['country'] is not None:
|
||||
country = pycountry.countries.get(alpha_2=net['country'])
|
||||
if country:
|
||||
if country := pycountry.countries.get(alpha_2=net['country']):
|
||||
country_name = country.name
|
||||
else:
|
||||
# May not always be an actual Country.
|
||||
@@ -45,8 +44,6 @@ class IPWhois:
|
||||
'Entity Type': 'Company'},
|
||||
{uid: {'Resolution': 'IPWhois', 'Notes': ''}}])
|
||||
if net['emails'] is not None:
|
||||
for email in net['emails']:
|
||||
return_result.append([{'Email Address': email,
|
||||
'Entity Type': 'Email Address'},
|
||||
{uid: {'Resolution': 'IPWhois', 'Notes': ''}}])
|
||||
return_result.extend([{'Email Address': email, 'Entity Type': 'Email Address'},
|
||||
{uid: {'Resolution': 'IPWhois', 'Notes': ''}}] for email in net['emails'])
|
||||
return return_result
|
||||
|
||||
@@ -18,22 +18,21 @@ class ImageToDevice:
|
||||
uid = entity['uid']
|
||||
index_of_child = len(return_result)
|
||||
image_path = Path(parameters['Project Files Directory']) / entity['File Path']
|
||||
if not (image_path.exists() and image_path.is_file()):
|
||||
if not image_path.exists() or not image_path.is_file():
|
||||
continue
|
||||
with open(image_path, 'rb') as image_file:
|
||||
my_image = Image(image_file)
|
||||
if my_image.has_exif is False:
|
||||
continue
|
||||
else:
|
||||
for tag in my_image.list_all():
|
||||
if tag == "make":
|
||||
return_result.append([{'Phrase': my_image.make,
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': 'ExifMetadata Device Manufacturer',
|
||||
'Notes': ''}}])
|
||||
if tag == "model":
|
||||
return_result.append([{'Phrase': my_image.model,
|
||||
'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'ExifMetadata Device Model',
|
||||
'Notes': ''}}])
|
||||
for tag in my_image.list_all():
|
||||
if tag == "make":
|
||||
return_result.append([{'Phrase': my_image.make,
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': 'ExifMetadata Device Manufacturer',
|
||||
'Notes': ''}}])
|
||||
elif tag == "model":
|
||||
return_result.append([{'Phrase': my_image.model,
|
||||
'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'ExifMetadata Device Model',
|
||||
'Notes': ''}}])
|
||||
return return_result
|
||||
|
||||
@@ -17,18 +17,17 @@ class ImageToGeoLocation:
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
image_path = Path(parameters['Project Files Directory']) / entity['File Path']
|
||||
if not (image_path.exists() and image_path.is_file()):
|
||||
if not image_path.exists() or not image_path.is_file():
|
||||
continue
|
||||
with open(image_path, 'rb') as image_file:
|
||||
my_image = Image(image_file)
|
||||
if my_image.has_exif is False:
|
||||
continue
|
||||
else:
|
||||
for tag in my_image.list_all():
|
||||
if tag == "gps_latitude":
|
||||
return_result.append([{'Label': "Location of"+str(entity[list(entity)[1]].strip()),
|
||||
'Latitude': my_image.gps_latitude,
|
||||
'Longitude': my_image.gps_longitude,
|
||||
'Entity Type': 'GeoCoordinates'},
|
||||
{uid: {'Resolution': 'GeoCoordinates', 'Notes': ''}}])
|
||||
return_result.extend([{'Label': f"Location of {str(entity[list(entity)[1]].strip())}",
|
||||
'Latitude': my_image.gps_latitude,
|
||||
'Longitude': my_image.gps_longitude,
|
||||
'Entity Type': 'GeoCoordinates'},
|
||||
{uid: {'Resolution': 'GeoCoordinates', 'Notes': ''}}]
|
||||
for tag in my_image.list_all() if tag == "gps_latitude")
|
||||
|
||||
return return_result
|
||||
|
||||
@@ -19,7 +19,11 @@ class JSCodeExtractor:
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
from playwright.sync_api import sync_playwright, Error
|
||||
from base64 import b64decode
|
||||
from pathlib import Path
|
||||
import re
|
||||
import contextlib
|
||||
|
||||
playwrightPath = Path(parameters['Playwright Firefox'])
|
||||
returnResults = []
|
||||
requestUrlsParsed = set()
|
||||
|
||||
@@ -27,7 +31,7 @@ class JSCodeExtractor:
|
||||
pubRegex = re.compile(r'\bca-pub-\d{1,16}\b', re.IGNORECASE)
|
||||
gtmRegex = re.compile(r'\bGTM-[A-Z\d]{1,7}\b')
|
||||
gRegex = re.compile(r'\bG-[A-Z\d]{1,15}\b', re.IGNORECASE)
|
||||
qualtricsRegex = re.compile(r'\bQ_(?:Z|S)ID=\w*\b')
|
||||
qualtricsRegex = re.compile(r'\bQ_[ZS]ID=\w*\b')
|
||||
pingdomRegex = re.compile(r'\bpa-[a-fA-F\d]{24}.js\b$')
|
||||
mPulseRegex = re.compile(r'go-mpulse.net/boomerang/[A-Z\d]{5}(?:-[A-Z\d]{5}){4}\b')
|
||||
contextWebRegex = re.compile(r'\.contextweb\.com.*token=.*')
|
||||
@@ -48,129 +52,130 @@ class JSCodeExtractor:
|
||||
brightcoveRegex = re.compile(r'metrics\.brightcove\.com/.*/tracker\?.*&account=[^&]*')
|
||||
|
||||
def GetTrackingCodes(pageUid, requestUrl) -> None:
|
||||
if requestUrl not in requestUrlsParsed:
|
||||
requestUrlsParsed.add(requestUrl)
|
||||
for uaCode in uaRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': uaCode,
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Google UA Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for pubCode in pubRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': pubCode,
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Google AdSense ca-pub Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for gtmCode in gtmRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': gtmCode,
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Google GTM Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for gCode in gRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': gCode,
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Google G Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for qCode in qualtricsRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': qCode[6:],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Qualtrics Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for pCode in pingdomRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': pCode[:-3],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Pingdom Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for mCode in mPulseRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': mCode.split('/')[-1],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'mPulse Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for cCode in contextWebRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': cCode.split('token=')[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'ContextWeb Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for fCode in facebookRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': fCode.split('id=')[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Facebook Tracking Pixel Code',
|
||||
'Notes': ''}}])
|
||||
for mapsCode in googleMapsRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': mapsCode.split('client=', 1)[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Google Maps Client Code',
|
||||
'Notes': ''}}])
|
||||
for marketoCode in marketoRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': marketoCode.split('aid=')[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Marketo Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for vwoCode in visualWebsiteOptimizerRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': vwoCode.split('a=')[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Visual Website Optimizer Tracking User ID',
|
||||
'Notes': ''}}])
|
||||
for oCode in optimizeRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': oCode.split('id=')[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Google Optimize ID',
|
||||
'Notes': ''}}])
|
||||
for mmCode in markMonitorRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': mmCode.split('adv=')[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Mark Monitor Tracking ID',
|
||||
'Notes': ''}}])
|
||||
for zCode in zendeskRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': zCode.split('key=')[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Zendesk ID',
|
||||
'Notes': ''}}])
|
||||
for qsCode in quantServeRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': qsCode.split('/pixel/')[1].split('.gif')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'QuantServe Tracking Pixel ID',
|
||||
'Notes': ''}}])
|
||||
for clCode in cookieLawRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': clCode.split('/')[2],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'CookieLaw Website ID',
|
||||
'Notes': ''}}])
|
||||
for otCode in oneTagRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': otCode.split('/')[1],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'OneTag Tracking ID',
|
||||
'Notes': ''}}])
|
||||
for beCode in bounceExchangeRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': beCode.split('/')[1],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'BounceExchange Tracking ID',
|
||||
'Notes': ''}}])
|
||||
for pushlyCode in pushlyRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': pushlyCode.split('domain_key=')[1],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Pushly Website ID',
|
||||
'Notes': ''}}])
|
||||
for aCode in akamaiRegex.findall(requestUrl):
|
||||
encodedTracking = aCode.split('a=', 1)[1]
|
||||
encodedTracking = encodedTracking.replace('%3D', '=')
|
||||
decodedTracking = b64decode(encodedTracking).decode('utf-8').split('t=')[1].split('&')[0]
|
||||
returnResults.append([{'Phrase': decodedTracking,
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Akamai Website ID',
|
||||
'Notes': 'SHA-1 Sum'}}])
|
||||
for dCode in demdexRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': dCode.split('d_orgid=')[1],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'DemDex (Adobe) Website ID',
|
||||
'Notes': ''}}])
|
||||
for bCode in brightcoveRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': bCode.split('account=')[1],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'BrightCove Website ID',
|
||||
'Notes': ''}}])
|
||||
if requestUrl in requestUrlsParsed:
|
||||
return
|
||||
requestUrlsParsed.add(requestUrl)
|
||||
for uaCode in uaRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': uaCode,
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Google UA Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for pubCode in pubRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': pubCode,
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Google AdSense ca-pub Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for gtmCode in gtmRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': gtmCode,
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Google GTM Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for gCode in gRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': gCode,
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Google G Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for qCode in qualtricsRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': qCode[6:],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Qualtrics Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for pCode in pingdomRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': pCode[:-3],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Pingdom Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for mCode in mPulseRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': mCode.split('/')[-1],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'mPulse Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for cCode in contextWebRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': cCode.split('token=')[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'ContextWeb Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for fCode in facebookRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': fCode.split('id=')[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Facebook Tracking Pixel Code',
|
||||
'Notes': ''}}])
|
||||
for mapsCode in googleMapsRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': mapsCode.split('client=', 1)[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Google Maps Client Code',
|
||||
'Notes': ''}}])
|
||||
for marketoCode in marketoRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': marketoCode.split('aid=')[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Marketo Tracking Code',
|
||||
'Notes': ''}}])
|
||||
for vwoCode in visualWebsiteOptimizerRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': vwoCode.split('a=')[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Visual Website Optimizer Tracking User ID',
|
||||
'Notes': ''}}])
|
||||
for oCode in optimizeRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': oCode.split('id=')[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Google Optimize ID',
|
||||
'Notes': ''}}])
|
||||
for mmCode in markMonitorRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': mmCode.split('adv=')[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Mark Monitor Tracking ID',
|
||||
'Notes': ''}}])
|
||||
for zCode in zendeskRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': zCode.split('key=')[1].split('&')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Zendesk ID',
|
||||
'Notes': ''}}])
|
||||
for qsCode in quantServeRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': qsCode.split('/pixel/')[1].split('.gif')[0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'QuantServe Tracking Pixel ID',
|
||||
'Notes': ''}}])
|
||||
for clCode in cookieLawRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': clCode.split('/')[2],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'CookieLaw Website ID',
|
||||
'Notes': ''}}])
|
||||
for otCode in oneTagRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': otCode.split('/')[1],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'OneTag Tracking ID',
|
||||
'Notes': ''}}])
|
||||
for beCode in bounceExchangeRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': beCode.split('/')[1],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'BounceExchange Tracking ID',
|
||||
'Notes': ''}}])
|
||||
for pushlyCode in pushlyRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': pushlyCode.split('domain_key=')[1],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Pushly Website ID',
|
||||
'Notes': ''}}])
|
||||
for aCode in akamaiRegex.findall(requestUrl):
|
||||
encodedTracking = aCode.split('a=', 1)[1]
|
||||
encodedTracking = encodedTracking.replace('%3D', '=')
|
||||
decodedTracking = b64decode(encodedTracking).decode('utf-8').split('t=')[1].split('&')[0]
|
||||
returnResults.append([{'Phrase': decodedTracking,
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'Akamai Website ID',
|
||||
'Notes': 'SHA-1 Sum'}}])
|
||||
for dCode in demdexRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': dCode.split('d_orgid=')[1],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'DemDex (Adobe) Website ID',
|
||||
'Notes': ''}}])
|
||||
for bCode in brightcoveRegex.findall(requestUrl):
|
||||
returnResults.append([{'Phrase': bCode.split('account=')[1],
|
||||
'Entity Type': 'Phrase'},
|
||||
{pageUid: {'Resolution': 'BrightCove Website ID',
|
||||
'Notes': ''}}])
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.firefox.launch()
|
||||
browser = p.firefox.launch(executable_path=playwrightPath)
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0'
|
||||
@@ -194,16 +199,12 @@ class JSCodeExtractor:
|
||||
if url is None:
|
||||
continue
|
||||
if not url.startswith('http://') and not url.startswith('https://'):
|
||||
url = 'http://' + url
|
||||
url = f'http://{url}'
|
||||
|
||||
try:
|
||||
with contextlib.suppress(Error):
|
||||
pageJS.goto(url, wait_until="networkidle")
|
||||
except Error:
|
||||
pass
|
||||
try:
|
||||
with contextlib.suppress(Error):
|
||||
pageNoJS.goto(url, wait_until="networkidle")
|
||||
except Error:
|
||||
pass
|
||||
pageJS.close()
|
||||
pageNoJS.close()
|
||||
browser.close()
|
||||
|
||||
110
Core/Resolutions/Core/LongANStringExtractor.py
Normal file
110
Core/Resolutions/Core/LongANStringExtractor.py
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Credit to @cyb_detective:
|
||||
https://medium.com/@cyb_detective/20-regular-expressions-examples-to-search-for-data-related-to-cryptocurrencies-43e31dd4a5dc
|
||||
"""
|
||||
|
||||
|
||||
class LongANStringExtractor:
|
||||
# A string that is treated as the name of this resolution.
|
||||
name = "Extract Long Alphanumeric Strings"
|
||||
|
||||
category = "Website Information"
|
||||
|
||||
# A string that describes this resolution.
|
||||
description = "Returns patterns matching common cryptocurrency address formats on a website."
|
||||
|
||||
originTypes = {'Domain', 'Website'}
|
||||
|
||||
resultTypes = {'Phrase'}
|
||||
|
||||
parameters = {'Minimum Length': {'description': 'Specify the minimum length an alphanumeric string has to have '
|
||||
'to be extracted.',
|
||||
'type': 'String',
|
||||
'value': '',
|
||||
'default': '20'
|
||||
}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
from playwright.sync_api import sync_playwright, TimeoutError, Error
|
||||
from bs4 import BeautifulSoup
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
playwrightPath = Path(parameters['Playwright Chromium'])
|
||||
try:
|
||||
minLength = int(parameters['Minimum Length'])
|
||||
if minLength < 1:
|
||||
raise ValueError('Invalid min length specified.')
|
||||
except ValueError:
|
||||
return "Invalid Minimum Length specified."
|
||||
|
||||
returnResults = []
|
||||
|
||||
matchPattern = re.compile(r"\b[a-zA-Z0-9_.-]{" + str(minLength) + r",}\b")
|
||||
|
||||
# The software can deduplicate, but handling it here is better.
|
||||
allPatterns = set()
|
||||
|
||||
def extractStrings(currentUID: str, site: str):
|
||||
page = context.new_page()
|
||||
pageResolved = False
|
||||
for _ in range(3):
|
||||
try:
|
||||
page.goto(site, wait_until="networkidle", timeout=10000)
|
||||
pageResolved = True
|
||||
break
|
||||
except TimeoutError:
|
||||
pass
|
||||
except Error:
|
||||
break
|
||||
if not pageResolved:
|
||||
# Last chance for this to work; some pages have issues with the "networkidle" trigger.
|
||||
try:
|
||||
page.goto(site, wait_until="load", timeout=10000)
|
||||
except Error:
|
||||
return
|
||||
|
||||
soupContents = BeautifulSoup(page.content(), 'lxml')
|
||||
# Remove <span> and <noscript> tags.
|
||||
while True:
|
||||
try:
|
||||
soupContents.noscript.extract()
|
||||
except AttributeError:
|
||||
break
|
||||
while True:
|
||||
try:
|
||||
soupContents.span.extract()
|
||||
except AttributeError:
|
||||
break
|
||||
siteContent = soupContents.get_text()
|
||||
|
||||
stringMatches = matchPattern.findall(siteContent)
|
||||
for potentialMatch in stringMatches:
|
||||
if potentialMatch not in allPatterns:
|
||||
allPatterns.add(potentialMatch)
|
||||
returnResults.append([{'Phrase': potentialMatch,
|
||||
'Entity Type': 'Phrase'},
|
||||
{currentUID: {'Resolution': 'Long alphanumeric string',
|
||||
'Notes': ''}}])
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(executable_path=playwrightPath)
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
'Chrome/101.0.4951.54 Safari/537.36'
|
||||
)
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
url = entity.get('URL') if entity.get('Entity Type', '') == 'Website' else \
|
||||
entity.get('Domain Name', None)
|
||||
if url is None:
|
||||
continue
|
||||
if not url.startswith('http://') and not url.startswith('https://'):
|
||||
url = f'http://{url}'
|
||||
extractStrings(uid, url)
|
||||
browser.close()
|
||||
|
||||
return returnResults
|
||||
37
Core/Resolutions/Core/NPMJSSearch.py
Normal file
37
Core/Resolutions/Core/NPMJSSearch.py
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class NPMJSSearch:
|
||||
name = "Find NPM organization"
|
||||
category = "Online Identity"
|
||||
description = "Find a collective's npmjs organization page."
|
||||
originTypes = {'Phrase', 'Company', 'Organization'}
|
||||
resultTypes = {'Website'}
|
||||
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0'}
|
||||
url_base = 'https://www.npmjs.com/org/'
|
||||
|
||||
returnResults = []
|
||||
|
||||
for entity in entityJsonList:
|
||||
primaryField = entity[list(entity)[1]].lower()
|
||||
|
||||
string_checks = {''.join(primaryField.split(' '))}
|
||||
string_checks.add('_'.join(primaryField.split(' ')))
|
||||
string_checks.add('-'.join(primaryField.split(' ')))
|
||||
|
||||
for check in string_checks:
|
||||
check_url = url_base + check
|
||||
request = requests.head(check_url, headers=headers)
|
||||
if request.status_code == 200:
|
||||
returnResults.append([{'URL': check_url,
|
||||
'Entity Type': 'Website'},
|
||||
{entity['uid']: {'Resolution': 'NPMJS Org',
|
||||
'Notes': ''}}])
|
||||
|
||||
return returnResults
|
||||
@@ -24,8 +24,11 @@ class PhoneNumbersExtractor:
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
from playwright.sync_api import sync_playwright, TimeoutError, Error
|
||||
from bs4 import BeautifulSoup
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
playwrightPath = Path(parameters['Playwright Chromium'])
|
||||
|
||||
cleanTagsRegex = re.compile(r'<.*?>')
|
||||
phoneNumCharsExclusion = re.compile(r'[^ -+()\[\]\d]')
|
||||
|
||||
@@ -55,12 +58,11 @@ class PhoneNumbersExtractor:
|
||||
linksInAHref = soupContents.find_all('a')
|
||||
for tag in linksInAHref:
|
||||
newLink = tag.get('href', None)
|
||||
if newLink is not None:
|
||||
if newLink.startswith('tel:'):
|
||||
returnResults.append([{'Phone Number': newLink[4:],
|
||||
'Entity Type': 'Phone Number'},
|
||||
{currentUID: {'Resolution': 'Phone Number Found',
|
||||
'Notes': ''}}])
|
||||
if newLink is not None and newLink.startswith('tel:'):
|
||||
returnResults.append([{'Phone Number': newLink[4:],
|
||||
'Entity Type': 'Phone Number'},
|
||||
{currentUID: {'Resolution': 'Phone Number Found',
|
||||
'Notes': ''}}])
|
||||
|
||||
textTags = soupContents.find_all('p')
|
||||
for tag in textTags:
|
||||
@@ -75,7 +77,7 @@ class PhoneNumbersExtractor:
|
||||
'Notes': ''}}])
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
browser = p.chromium.launch(executable_path=playwrightPath)
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
@@ -88,7 +90,7 @@ class PhoneNumbersExtractor:
|
||||
if url is None:
|
||||
continue
|
||||
if not url.startswith('http://') and not url.startswith('https://'):
|
||||
url = 'http://' + url
|
||||
url = f'http://{url}'
|
||||
extractTels(uid, url)
|
||||
browser.close()
|
||||
|
||||
|
||||
@@ -23,11 +23,10 @@ class PhraseSimilarity:
|
||||
entity_fields = []
|
||||
selection = parameters['Primary field or Notes']
|
||||
algorithm = parameters['Algorithm'].replace(" ", "_")
|
||||
if selection == 'Primary Field':
|
||||
for entity in entityJsonList:
|
||||
for entity in entityJsonList:
|
||||
if selection == 'Primary Field':
|
||||
entity_fields.append((entity['uid'], entity[list(entity)[1]].strip()))
|
||||
elif selection == 'Notes':
|
||||
for entity in entityJsonList:
|
||||
elif selection == 'Notes':
|
||||
entity_fields.append((entity['uid'], entity.get('Notes')))
|
||||
|
||||
if len(entity_fields) < 2:
|
||||
|
||||
@@ -59,10 +59,9 @@ class RegexMatch:
|
||||
|
||||
search_re = re.findall(search_param, text, flags=flagsToUse)
|
||||
|
||||
for regexMatch in search_re[:maxResults]:
|
||||
returnResults.append([{'Phrase': 'Regex Match: ' + regexMatch,
|
||||
'Entity Type': 'Phrase',
|
||||
'Notes': ''},
|
||||
{uid: {'Resolution': 'Regex String Match',
|
||||
'Notes': ''}}])
|
||||
returnResults.extend([{'Phrase': f'Regex Match: {regexMatch}',
|
||||
'Entity Type': 'Phrase',
|
||||
'Notes': ''},
|
||||
{uid: {'Resolution': 'Regex String Match', 'Notes': ''}}]
|
||||
for regexMatch in search_re[:maxResults])
|
||||
return returnResults
|
||||
|
||||
73
Core/Resolutions/Core/ReplacePhrase.py
Normal file
73
Core/Resolutions/Core/ReplacePhrase.py
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class ReplacePhrase:
|
||||
name = "Replace String in Phrase"
|
||||
category = "String Operations"
|
||||
description = "Replace a character, or sequence of characters in a Phrase with another character or sequence."
|
||||
originTypes = {'Phrase'}
|
||||
resultTypes = {'Phrase'}
|
||||
|
||||
parameters = {'Sequence to Remove': {'description': 'Specify the character or sequence of characters to replace.',
|
||||
'type': 'String',
|
||||
'value': ''
|
||||
},
|
||||
'Sequence to Insert': {'description': 'Specify the character or sequence of characters to replace '
|
||||
'the old character or sequence with. Enter the same character '
|
||||
'or sequence to delete the character or sequence instead.',
|
||||
'type': 'String',
|
||||
'value': ''
|
||||
},
|
||||
'Match Type': {'description': 'Specify whether the matching of characters to replace '
|
||||
'should be plain (as in, match characters as they were typed), '
|
||||
'case insensitive, or regex.',
|
||||
'type': 'SingleChoice',
|
||||
'value': {'Plain', 'Case Insensitive', 'Regex'},
|
||||
'default': 'Plain'
|
||||
},
|
||||
'Match Count': {'description': 'Specify the number of times to replace the specified character or '
|
||||
'sequence with the new sequence. Zero is unlimited times.',
|
||||
'type': 'String',
|
||||
'value': '0',
|
||||
'default': '0'
|
||||
}
|
||||
}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import re
|
||||
|
||||
returnResults = []
|
||||
|
||||
remove = parameters['Sequence to Remove']
|
||||
insert = parameters['Sequence to Insert']
|
||||
if insert == remove:
|
||||
insert = ''
|
||||
matchType = parameters['Match Type']
|
||||
|
||||
try:
|
||||
matchCount = int(parameters['Match Count'])
|
||||
if matchCount < 0:
|
||||
return []
|
||||
except ValueError:
|
||||
return "Invalid Match Count specified."
|
||||
|
||||
for entity in entityJsonList:
|
||||
primaryField = entity['Phrase']
|
||||
|
||||
if matchType == 'Plain':
|
||||
if matchCount == 0:
|
||||
matchCount = -1
|
||||
primaryField = primaryField.replace(remove, insert, matchCount)
|
||||
elif matchType == 'Case Insensitive':
|
||||
pattern = re.compile(re.escape(remove), re.IGNORECASE)
|
||||
primaryField = pattern.sub(insert, primaryField, matchCount)
|
||||
else:
|
||||
pattern = re.compile(remove)
|
||||
primaryField = pattern.sub(insert, primaryField, matchCount)
|
||||
|
||||
returnResults.append([{'Phrase': primaryField,
|
||||
'Entity Type': 'Phrase'},
|
||||
{entity['uid']: {'Resolution': 'Replace characters',
|
||||
'Notes': ''}}])
|
||||
|
||||
return returnResults
|
||||
@@ -34,16 +34,14 @@ class TikTokVideoPublishDetails:
|
||||
|
||||
binString = "{0:b}".format(videoID)
|
||||
if len(binString) == 63:
|
||||
binString = '0' + binString
|
||||
binString = f'0{binString}'
|
||||
binString = int(binString[:32], 2)
|
||||
|
||||
UTCTimestamp = datetime.utcfromtimestamp(binString).isoformat() + '+00:00'
|
||||
UTCTimestamp = f'{datetime.utcfromtimestamp(binString).isoformat()}+00:00'
|
||||
|
||||
returnResults.append([{'Date': UTCTimestamp,
|
||||
'Entity Type': 'Date'},
|
||||
{uid: {'Resolution': 'Video Publish Date', 'Notes': ''}}])
|
||||
returnResults.append([{'User Name': username,
|
||||
'Entity Type': 'Social Media Handle'},
|
||||
{uid: {'Resolution': 'Published By', 'Notes': ''}}])
|
||||
returnResults.extend(([{'Date': UTCTimestamp, 'Entity Type': 'Date'},
|
||||
{uid: {'Resolution': 'Video Publish Date', 'Notes': ''}}],
|
||||
[{'User Name': username, 'Entity Type': 'Social Media Handle'},
|
||||
{uid: {'Resolution': 'Published By', 'Notes': ''}}]))
|
||||
|
||||
return returnResults
|
||||
|
||||
@@ -11,10 +11,11 @@ class WebsiteFromPhrase:
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import contextlib
|
||||
import re
|
||||
import tldextract
|
||||
|
||||
websiteRegex = re.compile(r"""^https?://(\S(?<!\.)){1,63}(\.(\S(?<!\.)){1,63})+$""")
|
||||
websiteRegex = re.compile(r"""^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$""")
|
||||
wordChar = re.compile(r'\w')
|
||||
|
||||
returnResults = []
|
||||
@@ -25,14 +26,11 @@ class WebsiteFromPhrase:
|
||||
while wordChar.match(entityChunk[-1]) is None:
|
||||
entityChunk = entityChunk[:-1]
|
||||
if websiteRegex.match(entityChunk):
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
tldObject = tldextract.extract(entityChunk)
|
||||
if tldObject.suffix != '':
|
||||
returnResults.append([{'URL': entityChunk,
|
||||
'Entity Type': 'Website'},
|
||||
{entity['uid']: {'Resolution': 'Phrase To Website',
|
||||
'Notes': ''}}])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return returnResults
|
||||
|
||||
@@ -9,7 +9,8 @@ class WordCounter:
|
||||
resultTypes = {'Phrase'}
|
||||
parameters = {'Primary field or Notes': {'description': 'Choose Either Primary field or Notes',
|
||||
'type': 'SingleChoice',
|
||||
'value': {'Notes', 'Primary Field'}}}
|
||||
'value': {'Notes', 'Primary Field'},
|
||||
'default': 'Notes'}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import re
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
|
||||
import contextlib
|
||||
from typing import Union
|
||||
import re
|
||||
from typing import Union, Optional
|
||||
from glob import glob
|
||||
|
||||
import networkx as nx
|
||||
import re
|
||||
from datetime import timezone
|
||||
from defusedxml.ElementTree import parse
|
||||
from datetime import datetime
|
||||
@@ -16,10 +17,42 @@ from ast import literal_eval
|
||||
from base64 import b64decode
|
||||
from dateutil import parser
|
||||
|
||||
from PySide6.QtCore import QByteArray, QSize, QUrl, Qt
|
||||
from PIL import Image
|
||||
from PIL.ImageQt import ImageQt
|
||||
from PySide6.QtCore import QByteArray, QBuffer, QIODevice, QSize, QUrl, Qt
|
||||
from PySide6 import QtWidgets, QtGui
|
||||
|
||||
from Core.Interface import Stylesheets
|
||||
|
||||
def resizePictureFromBuffer(picBuffer: QByteArray, newSize: tuple) -> QByteArray:
|
||||
"""
|
||||
newSize: First is width, second is height.
|
||||
"""
|
||||
picBufferData = picBuffer.data()
|
||||
if picBufferData.startswith(b'<svg ') or picBufferData.startswith(b'<?xml '):
|
||||
return QByteArray(resizeSVG(picBufferData, newSize))
|
||||
originalImage = QtGui.QImage()
|
||||
originalImage.loadFromData(picBuffer)
|
||||
newImage = originalImage.scaled(newSize[0], newSize[1])
|
||||
|
||||
pictureByteArray = QByteArray()
|
||||
imageBuffer = QBuffer(pictureByteArray)
|
||||
imageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||
newImage.save(imageBuffer, "PNG")
|
||||
imageBuffer.close()
|
||||
|
||||
return pictureByteArray
|
||||
|
||||
|
||||
def resizeSVG(byteString: bytes, resize: tuple):
|
||||
bytesWidth = str(resize[0]).encode('UTF-8')
|
||||
bytesHeight = str(resize[1]).encode('UTF-8')
|
||||
widthRegex = re.compile(b' width="\d*" ')
|
||||
for widthMatches in widthRegex.findall(byteString):
|
||||
byteString = byteString.replace(widthMatches, b' ')
|
||||
heightRegex = re.compile(b' height="\d*" ')
|
||||
for heightMatches in heightRegex.findall(byteString):
|
||||
byteString = byteString.replace(heightMatches, b' ')
|
||||
return byteString.replace(b'<svg ', b'<svg height="%b" width="%b" ' % (bytesHeight, bytesWidth), 1)
|
||||
|
||||
|
||||
class ResourceHandler:
|
||||
@@ -28,67 +61,95 @@ class ResourceHandler:
|
||||
return self.icons[iconName]
|
||||
|
||||
# Load all resources needed.
|
||||
def __init__(self, mainWindow, messageHandler) -> None:
|
||||
def __init__(self, mainWindow) -> None:
|
||||
self.mainWindow = mainWindow
|
||||
self.messageHandler = messageHandler
|
||||
self.programBaseDirPath = Path(self.mainWindow.SETTINGS.value("Program/BaseDir"))
|
||||
self.entityCategoryList = {}
|
||||
self.moduleAssetPaths = []
|
||||
|
||||
self.icons = {"uploading": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "Uploading.png"),
|
||||
"uploaded": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "Uploaded.png"),
|
||||
"upArrow": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "UpArrow.png"),
|
||||
"downArrow": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "DownArrow.png"),
|
||||
"isolatedNodes": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "SelectIsolated.png"),
|
||||
"addCanvas": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "Add_Canvas.png"),
|
||||
"generateReport": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "Generate_Report.png"),
|
||||
"leafNodes": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "SelectLeaf.png"),
|
||||
"nonIsolatedNodes": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "SelectNonIsolated.png"),
|
||||
"rootNodes": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "SelectRoot.png"),
|
||||
"split": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "Split.png"),
|
||||
"merge": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "Merge.png"),
|
||||
"shortestPath": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "ShortestPath.png"),
|
||||
"drawLink": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "DrawLink.png"),
|
||||
"rearrange": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "RearrangeGraph.png"),
|
||||
"colorPicker": str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) /
|
||||
"Resources" / "Icons" / "ColorPicker.png"),
|
||||
self.icons = {"uploading": str(self.programBaseDirPath / "Resources" / "Icons" / "Uploading.png"),
|
||||
"uploaded": str(self.programBaseDirPath / "Resources" / "Icons" / "Uploaded.png"),
|
||||
"upArrow": str(self.programBaseDirPath / "Resources" / "Icons" / "UpArrow.png"),
|
||||
"downArrow": str(self.programBaseDirPath / "Resources" / "Icons" / "DownArrow.png"),
|
||||
"isolatedNodes": str(self.programBaseDirPath / "Resources" / "Icons" / "SelectIsolated.png"),
|
||||
"addCanvas": str(self.programBaseDirPath / "Resources" / "Icons" / "Add_Canvas.png"),
|
||||
"generateReport": str(self.programBaseDirPath / "Resources" / "Icons" / "Generate_Report.png"),
|
||||
"leafNodes": str(self.programBaseDirPath / "Resources" / "Icons" / "SelectLeaf.png"),
|
||||
"nonIsolatedNodes": str(self.programBaseDirPath / "Resources" / "Icons" /
|
||||
"SelectNonIsolated.png"),
|
||||
"rootNodes": str(self.programBaseDirPath / "Resources" / "Icons" / "SelectRoot.png"),
|
||||
"split": str(self.programBaseDirPath / "Resources" / "Icons" / "Split.png"),
|
||||
"merge": str(self.programBaseDirPath / "Resources" / "Icons" / "Merge.png"),
|
||||
"shortestPath": str(self.programBaseDirPath / "Resources" / "Icons" / "ShortestPath.png"),
|
||||
"drawLink": str(self.programBaseDirPath / "Resources" / "Icons" / "DrawLink.png"),
|
||||
"rearrange": str(self.programBaseDirPath / "Resources" / "Icons" / "RearrangeGraph.png"),
|
||||
"colorPicker": str(self.programBaseDirPath / "Resources" / "Icons" / "ColorPicker.png"),
|
||||
}
|
||||
|
||||
self.banners = {f"{bannerPath.split('Banner_')[-1].split('.')[0]}": str(bannerPath)
|
||||
for bannerPath in glob(str(self.programBaseDirPath / "Resources" / "Icons" / "Banner_*.svg"))}
|
||||
# These are not meant to be strict - just restrictive enough such that users don't put in utter nonsense.
|
||||
# Note that regex isn't always the best way of validating fields, but it should be good enough for our
|
||||
# purposes.
|
||||
self.checks = {'Email': re.compile(r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"""),
|
||||
'Phonenumber': re.compile(r"""^(\+|00)?[0-9\(\) \-]{3,32}$"""),
|
||||
'String': re.compile(r""".+"""),
|
||||
'URL': re.compile(r"""^(?:(?:http|ftp)s?|file)://(\S(?<!\.)){1,63}(\.(\S(?<!\.)){1,63})+$"""),
|
||||
'Onion': re.compile(r"""^https?://\w{56}\.onion/?(\S(?<!\.))*(\.(\S(?<!\.))*)?$"""),
|
||||
'Domain': re.compile(r"""^(\S(?<!\.)(?!/)(?<!/)){1,63}(\.(\S(?<!\.)(?!/)(?<!/)){1,63})+$"""),
|
||||
'Float': re.compile(r"""^([-+])?(\d|\.(?=\d))+$"""),
|
||||
'WordString': re.compile(r"""^\D+$"""),
|
||||
'Numbers': re.compile(r"""^\d+$"""),
|
||||
'IPv4': re.compile(r"""^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$"""),
|
||||
'IPv6': re.compile(r"""^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$"""),
|
||||
'MAC': re.compile(r"""^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"""),
|
||||
'ASN': re.compile(r"""^(AS)?\d+$"""),
|
||||
'CUSIP': re.compile(r"""^[a-zA-Z0-9]{9}$"""),
|
||||
'EIN': re.compile(r"""^\d{2}-?\d{7}$"""),
|
||||
'LEIID': re.compile(r"""^[a-zA-Z0-9]{20}$"""),
|
||||
'ISINID': re.compile(r"""^[a-zA-Z0-9]{2}-?[a-zA-Z0-9]{9}-?[a-zA-Z0-9]$"""),
|
||||
'SIC/NAICS': re.compile(r"""^[0-9]{4,6}$""")}
|
||||
self.checks = {'Email': re.compile(
|
||||
r"""(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)])"""),
|
||||
'Phonenumber': re.compile(r"""^(\+|00)?[0-9() \-]{3,32}$"""),
|
||||
'String': re.compile(r""".+"""),
|
||||
'URL': re.compile(r"""[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)"""),
|
||||
'Onion': re.compile(r"""^https?://\w{56}\.onion/?(\S(?<!\.))*(\.(\S(?<!\.))*)?$"""),
|
||||
'Domain': re.compile(r"""^(\S(?<!\.)(?!/)(?<!/)){1,63}(\.(\S(?<!\.)(?!/)(?<!/)){1,63})+$"""),
|
||||
'Float': re.compile(r"""^([-+])?(\d|\.(?=\d))+$"""),
|
||||
'WordString': re.compile(r"""^\D+$"""),
|
||||
'Numbers': re.compile(r"""^\d+$"""),
|
||||
'IPv4': re.compile(r"""^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$"""),
|
||||
'IPv6': re.compile(
|
||||
r"""^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$"""),
|
||||
'MAC': re.compile(r"""^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"""),
|
||||
'ASN': re.compile(r"""^(AS)?\d+$"""),
|
||||
'CUSIP': re.compile(r"""^[a-zA-Z0-9]{9}$"""),
|
||||
'EIN': re.compile(r"""^\d{2}-?\d{7}$"""),
|
||||
'LEIID': re.compile(r"""^[a-zA-Z0-9]{20}$"""),
|
||||
'ISINID': re.compile(r"""^[a-zA-Z0-9]{2}-?[a-zA-Z0-9]{9}-?[a-zA-Z0-9]$"""),
|
||||
'SIC/NAICS': re.compile(r"""^[0-9]{4,6}$""")}
|
||||
|
||||
self.loadCoreEntities()
|
||||
self.loadModuleEntities(self.programBaseDirPath / "Core")
|
||||
|
||||
def getPictureFromFile(self, filePath: Path, resize: tuple = (0, 0)) -> Optional[QByteArray]:
|
||||
"""
|
||||
resize: tuple, first is width, second is height.
|
||||
"""
|
||||
try:
|
||||
with open(filePath, 'rb') as newIconFile:
|
||||
fileContents = newIconFile.read()
|
||||
if fileContents.startswith(b'<svg ') or fileContents.startswith(b'<?xml '):
|
||||
if resize != (0, 0):
|
||||
fileContents = resizeSVG(fileContents, resize)
|
||||
pictureByteArray = QByteArray(fileContents)
|
||||
else:
|
||||
image = Image.open(str(filePath))
|
||||
if resize != (0, 0):
|
||||
thumbnail = ImageQt(image.resize(resize))
|
||||
else:
|
||||
thumbnail = ImageQt(image)
|
||||
pictureByteArray = QByteArray()
|
||||
imageBuffer = QBuffer(pictureByteArray)
|
||||
|
||||
imageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||
|
||||
thumbnail.save(imageBuffer, "PNG")
|
||||
imageBuffer.close()
|
||||
except ValueError as ve:
|
||||
# Image type is unsupported (for ImageQt)
|
||||
# Supported types: 1, L, P, RGB, RGBA
|
||||
self.mainWindow.MESSAGEHANDLER.warning(f'Invalid Image selected: {str(ve)}', popUp=True)
|
||||
pictureByteArray = None
|
||||
|
||||
return pictureByteArray
|
||||
|
||||
def loadModuleBanners(self, modulePath: Path):
|
||||
assetsPath = modulePath / "Assets"
|
||||
self.banners.update({f"{bannerPath.split('Banner_')[-1].split('.')[0]}": str(bannerPath)
|
||||
for bannerPath in glob(str(assetsPath / "Banner_*.svg"))})
|
||||
|
||||
def getEntityCategories(self) -> list:
|
||||
return list(self.entityCategoryList)
|
||||
@@ -98,8 +159,7 @@ class ResourceHandler:
|
||||
for entity in self.entityCategoryList[category]:
|
||||
entityValue = self.entityCategoryList[category][entity]
|
||||
eList.append((self.getBareBonesEntityJson(entity),
|
||||
entityValue['Icon']
|
||||
))
|
||||
entityValue['Icon']))
|
||||
return eList
|
||||
|
||||
def getEntityAttributes(self, entityType) -> Union[None, list]:
|
||||
@@ -110,8 +170,8 @@ class ResourceHandler:
|
||||
aList.extend(iter(self.entityCategoryList[category][entityType]['Attributes']))
|
||||
break
|
||||
except KeyError:
|
||||
self.messageHandler.error("Attempted to get attributes for "
|
||||
"nonexistent entity type: " + str(entityType), True)
|
||||
self.mainWindow.MESSAGEHANDLER.error(
|
||||
f"Attempted to get attributes for nonexistent entity type: {entityType}", True)
|
||||
return None
|
||||
return aList
|
||||
|
||||
@@ -167,13 +227,24 @@ class ResourceHandler:
|
||||
result = attrCheck.findall(attribute)
|
||||
return len(result) == 1
|
||||
|
||||
def addRecognisedEntityTypes(self, entityFile: Path) -> bool:
|
||||
def getIconPathForIconFile(self, iconFile: str) -> Union[None, Path]:
|
||||
iconPath = self.programBaseDirPath / "Resources" / "Icons" / iconFile
|
||||
if iconPath.exists():
|
||||
return iconPath
|
||||
for assetPath in self.moduleAssetPaths:
|
||||
iconPath = assetPath / iconFile
|
||||
if iconPath.exists():
|
||||
return iconPath
|
||||
return self.programBaseDirPath / "Resources" / "Icons" / "Default.svg"
|
||||
|
||||
def addRecognisedEntityTypes(self, entityFile: Path) -> list:
|
||||
entityTypesAdded = []
|
||||
try:
|
||||
tree = parse(entityFile, forbid_dtd=True, forbid_entities=True, forbid_external=True)
|
||||
except Exception as exc:
|
||||
self.mainWindow.MESSAGEHANDLER.warning('Error occurred when loading entities from '
|
||||
+ str(entityFile) + ': ' + str(exc) + ', skipping.')
|
||||
return False
|
||||
self.mainWindow.MESSAGEHANDLER.warning(
|
||||
f'Error occurred when loading entities from {entityFile}: {exc}, skipping.')
|
||||
return []
|
||||
|
||||
root = tree.getroot()
|
||||
|
||||
@@ -208,26 +279,28 @@ class ResourceHandler:
|
||||
self.entityCategoryList[category] = {}
|
||||
self.entityCategoryList[category][entityName] = {
|
||||
'Attributes': attributesDict,
|
||||
'Icon': str(Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Resources" / "Icons" / icon)}
|
||||
'Icon': str(self.getIconPathForIconFile(icon))}
|
||||
entityTypesAdded.append(f'{category}/{entityName}')
|
||||
except (KeyError, AttributeError) as err:
|
||||
# Ignore malformed entities
|
||||
self.messageHandler.error(f'Error: {str(err)}', popUp=False)
|
||||
self.mainWindow.MESSAGEHANDLER.error(f'Error: {str(err)}', popUp=False)
|
||||
continue
|
||||
return True
|
||||
return entityTypesAdded
|
||||
|
||||
def loadCoreEntities(self) -> None:
|
||||
entDir = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Core" / "Entities"
|
||||
for entFile in listdir(entDir):
|
||||
if entFile.endswith('.xml'):
|
||||
self.addRecognisedEntityTypes(entDir / entFile)
|
||||
|
||||
def loadModuleEntities(self) -> None:
|
||||
entDir = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Modules"
|
||||
for module in listdir(entDir):
|
||||
for entFile in listdir(entDir / module):
|
||||
def loadModuleEntities(self, modulePath: Path) -> list:
|
||||
entitiesPath = modulePath / 'Entities'
|
||||
allModuleEntitiesAdded = []
|
||||
if entitiesPath.exists():
|
||||
for entFile in listdir(entitiesPath):
|
||||
if entFile.endswith('.xml'):
|
||||
self.addRecognisedEntityTypes(
|
||||
entDir / module / entFile)
|
||||
allModuleEntitiesAdded += self.addRecognisedEntityTypes(entitiesPath / entFile)
|
||||
return allModuleEntitiesAdded
|
||||
|
||||
def loadModuleAssets(self, modulePath: Path):
|
||||
moduleAssetsPath = modulePath / "Assets"
|
||||
if moduleAssetsPath.exists():
|
||||
self.moduleAssetPaths.append(moduleAssetsPath)
|
||||
self.loadModuleBanners(modulePath)
|
||||
|
||||
def getEntityJson(self, entityType: str, jsonData=None) -> Union[dict, None]:
|
||||
eJson = {'uid': str(uuid4())}
|
||||
@@ -240,7 +313,8 @@ class ResourceHandler:
|
||||
eJson[attribute] = self.entityCategoryList[category][entityType]['Attributes'][attribute][0]
|
||||
break
|
||||
except KeyError:
|
||||
self.messageHandler.error(f"Attempted to get attributes for malformed entity type: {entityType}", True)
|
||||
self.mainWindow.MESSAGEHANDLER.error(
|
||||
f"Attempted to get attributes for malformed entity type: {entityType}", True)
|
||||
return None
|
||||
eJson['Entity Type'] = entityType
|
||||
eJson['Date Created'] = None
|
||||
@@ -277,8 +351,8 @@ class ResourceHandler:
|
||||
if self.entityCategoryList[category][entityType]['Attributes'][attribute][2]:
|
||||
return attribute
|
||||
except KeyError:
|
||||
self.messageHandler.error(f"Attempted to get primary attribute for malformed entity type: {entityType}",
|
||||
True)
|
||||
self.mainWindow.MESSAGEHANDLER.error(
|
||||
f"Attempted to get primary attribute for malformed entity type: {entityType}", True)
|
||||
return None
|
||||
|
||||
def getBareBonesEntityJson(self, entityType: str) -> Union[dict, None]:
|
||||
@@ -290,7 +364,8 @@ class ResourceHandler:
|
||||
eJson[attribute] = self.entityCategoryList[category][entityType]['Attributes'][attribute][0]
|
||||
break
|
||||
except KeyError:
|
||||
self.messageHandler.error(f"Attempted to get attributes for malformed entity type: {entityType}", True)
|
||||
self.mainWindow.MESSAGEHANDLER.error(
|
||||
f"Attempted to get attributes for malformed entity type: {entityType}", True)
|
||||
return None
|
||||
eJson['Entity Type'] = entityType
|
||||
|
||||
@@ -324,8 +399,8 @@ class ResourceHandler:
|
||||
|
||||
return linkJson
|
||||
|
||||
def getEntityDefaultPicture(self, entityType) -> QByteArray:
|
||||
picture = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Resources" / "Icons" / "Default.svg"
|
||||
def getEntityDefaultPicture(self, entityType: str) -> QByteArray:
|
||||
picture = self.programBaseDirPath / "Resources" / "Icons" / "Default.svg"
|
||||
try:
|
||||
for category in self.entityCategoryList:
|
||||
if entityType in self.entityCategoryList[category]:
|
||||
@@ -334,19 +409,19 @@ class ResourceHandler:
|
||||
picture = entityPicture
|
||||
break
|
||||
except KeyError:
|
||||
self.messageHandler.warning("Attempted to get icon for "
|
||||
"nonexistent entity type: " + str(entityType), popUp=False)
|
||||
self.mainWindow.MESSAGEHANDLER.warning(
|
||||
f"Attempted to get icon for nonexistent entity type: {entityType}", popUp=False)
|
||||
finally:
|
||||
with open(picture, 'rb') as pictureFile:
|
||||
pictureContents = pictureFile.read()
|
||||
return QByteArray(pictureContents)
|
||||
|
||||
def getLinkPicture(self):
|
||||
picture = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Resources" / "Icons" / "Resolution.png"
|
||||
picture = self.programBaseDirPath / "Resources" / "Icons" / "Resolution.png"
|
||||
return QtGui.QIcon(str(picture)).pixmap(40, 40)
|
||||
|
||||
def getLinkArrowPicture(self):
|
||||
picture = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Resources" / "Icons" / "Right-Arrow.svg"
|
||||
picture = self.programBaseDirPath / "Resources" / "Icons" / "Right-Arrow.svg"
|
||||
return QtGui.QIcon(str(picture)).pixmap(40, 40)
|
||||
|
||||
def deconstructGraph(self, graph: nx.DiGraph) -> tuple:
|
||||
@@ -421,7 +496,7 @@ class FilePropertyInput(QtWidgets.QLineEdit):
|
||||
fileChosen = self.fileDialog.getOpenFileName(self,
|
||||
"Open File",
|
||||
str(Path.home()),
|
||||
options=QtWidgets.QFileDialog.DontUseNativeDialog)
|
||||
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog)
|
||||
self.setText(fileChosen[0])
|
||||
|
||||
|
||||
@@ -440,7 +515,6 @@ class SingleChoicePropertyInput(QtWidgets.QGroupBox):
|
||||
|
||||
for option in enforceOptionsSet:
|
||||
radioButton = QtWidgets.QRadioButton(option)
|
||||
radioButton.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
|
||||
if option == defaultOption:
|
||||
radioButton.setChecked(True)
|
||||
else:
|
||||
@@ -467,7 +541,6 @@ class MultiChoicePropertyInput(QtWidgets.QGroupBox):
|
||||
|
||||
for option in enforceOptionsSet:
|
||||
checkBox = QtWidgets.QCheckBox(option)
|
||||
checkBox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
||||
if option in defaultOptions:
|
||||
checkBox.setChecked(True)
|
||||
else:
|
||||
@@ -488,6 +561,7 @@ class MinSizeStackedLayout(QtWidgets.QStackedLayout):
|
||||
|
||||
https://stackoverflow.com/a/34300567
|
||||
"""
|
||||
|
||||
def sizeHint(self) -> QSize:
|
||||
return self.currentWidget().sizeHint()
|
||||
|
||||
@@ -536,7 +610,7 @@ class RichNotesEditor(QtWidgets.QTextBrowser):
|
||||
|
||||
def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None:
|
||||
potentialLink = self.anchorAt(ev.pos())
|
||||
if not potentialLink and ev.button() == QtGui.Qt.LeftButton:
|
||||
if not potentialLink and ev.button() == QtGui.Qt.MouseButton.LeftButton:
|
||||
self.startEditing()
|
||||
super(RichNotesEditor, self).mousePressEvent(ev)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from msgpack import dump
|
||||
from shutil import move
|
||||
from pathlib import Path
|
||||
from Core.PathHelper import is_path_exists_or_creatable_portable
|
||||
from PySide6.QtCore import QSettings
|
||||
|
||||
|
||||
class SettingsObject(dict):
|
||||
@@ -21,22 +22,67 @@ class SettingsObject(dict):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setValue("Program/BaseDir", "Unset") # dirname(abspath(getsourcefile(lambda:0))) + "/../" )
|
||||
self.setValue("Program/GraphLayout", "dot")
|
||||
self.setValue("Program/Graphics/EntityTextFontType", "Mono")
|
||||
self.setValue("Program/Graphics/EntityTextFontSize", "11")
|
||||
self.setValue("Program/Graphics/EntityTextFontBoldness", "700")
|
||||
self.setValue("Program/Graphics/LinkTextFontType", "Mono")
|
||||
self.setValue("Program/Graphics/LinkTextFontSize", "11")
|
||||
self.setValue("Program/Graphics/LinkTextFontBoldness", "700")
|
||||
self.setValue("Program/Graphics/EntityTextColor", "#000000") # RGB
|
||||
self.setValue("Program/Graphics/LinkTextColor", "#000000") # RGB
|
||||
self.setValue("Program/Graphics/LabelFade", "3")
|
||||
self.globalSettings = QSettings()
|
||||
self.globalSettings.setValue("Program/Version", "v1.6.5")
|
||||
self.globalSettings.setValue("Program/TOR Profile Location",
|
||||
self.globalSettings.value("Program/TOR Profile Location", ""))
|
||||
self.globalSettings.setValue("Program/BaseDir",
|
||||
self.globalSettings.value("Program/BaseDir", "Unset"))
|
||||
self.globalSettings.setValue("Program/Version Check Source",
|
||||
self.globalSettings.value("Program/Version Check Source",
|
||||
"https://api.github.com/repos/AccentuSoft/LinkScope_Client/releases/latest"))
|
||||
self.globalSettings.setValue("Program/Update Source",
|
||||
self.globalSettings.value("Program/Update Source",
|
||||
"https://github.com/AccentuSoft/LinkScope_Client/releases/latest/download/"))
|
||||
|
||||
# The value '20' equates to logging.INFO
|
||||
# It's not necessary to set this, but we will for
|
||||
# the sake of completeness
|
||||
self.globalSettings.setValue("Logging/Severity",
|
||||
self.globalSettings.value("Logging/Severity", "20"))
|
||||
self.globalSettings.setValue("Logging/Logfile",
|
||||
self.globalSettings.value("Logging/Logfile",
|
||||
str(Path.home() / 'LinkScope_logfile.log')))
|
||||
|
||||
self.globalSettings.setValue("Program/Graph Layout",
|
||||
self.globalSettings.value("Program/Graph Layout", "dot"))
|
||||
self.globalSettings.setValue("Program/Graphics/Entity Text Font Type",
|
||||
self.globalSettings.value("Program/Graphics/Entity Text Font Type", "Mono"))
|
||||
self.globalSettings.setValue("Program/Graphics/Entity Text Font Size",
|
||||
self.globalSettings.value("Program/Graphics/Entity Text Font Size", "11"))
|
||||
self.globalSettings.setValue("Program/Graphics/Entity Text Font Boldness",
|
||||
self.globalSettings.value("Program/Graphics/Entity Text Font Boldness", "700"))
|
||||
self.globalSettings.setValue("Program/Graphics/Link Text Font Type",
|
||||
self.globalSettings.value("Program/Graphics/Link Text Font Type", "Mono"))
|
||||
self.globalSettings.setValue("Program/Graphics/Link Text Font Size",
|
||||
self.globalSettings.value("Program/Graphics/Link Text Font Size", "11"))
|
||||
self.globalSettings.setValue("Program/Graphics/Link Text Font Boldness",
|
||||
self.globalSettings.value("Program/Graphics/Link Text Font Boldness", "700"))
|
||||
self.globalSettings.setValue("Program/Graphics/Entity Text Color",
|
||||
self.globalSettings.value("Program/Graphics/Entity Text Color", "#000000"))
|
||||
self.globalSettings.setValue("Program/Graphics/Link Text Color",
|
||||
self.globalSettings.value("Program/Graphics/Link Text Color", "#000000"))
|
||||
self.globalSettings.setValue("Program/Graphics/Label Fade Scroll Distance",
|
||||
self.globalSettings.value("Program/Graphics/Label Fade Scroll Distance", "3"))
|
||||
|
||||
self.globalSettings.setValue("Program/Usage/First Time Start",
|
||||
self.globalSettings.value("Program/Usage/First Time Start", 'true'))
|
||||
|
||||
self.globalSettings.setValue("Program/Sources/Sources List",
|
||||
self.globalSettings.value(
|
||||
"Program/Sources/Sources List",
|
||||
{}))
|
||||
|
||||
self.globalSettings.setValue("Program/Sources/Module Packs List",
|
||||
self.globalSettings.value(
|
||||
"Program/Sources/Module Packs List",
|
||||
{}))
|
||||
|
||||
self.setValue("Project/Name", "Untitled")
|
||||
self.setValue("Project/BaseDir", "")
|
||||
self.setValue("Project/FilesDir", "")
|
||||
# For any entity with a Path variable, this dictates whether a copy of the original is made or whether a
|
||||
# symlink is created. Symlinks require special permissions or developer mode in Windows however.
|
||||
# symlink is created. Symlinks however require special permissions or developer mode in Windows.
|
||||
# To ensure that the software works out-of-the-box on all platforms, the default is set to 'Copy'.
|
||||
self.setValue("Project/Symlink or Copy Materials", "Copy") # Values are 'Copy' or 'Symlink'.
|
||||
self.setValue("Project/Resolution Result Grouping Threshold", "15")
|
||||
@@ -46,20 +92,46 @@ class SettingsObject(dict):
|
||||
self.setValue("Project/Server/Project", "")
|
||||
self.setValue("Project/Server/Collectors", "{}")
|
||||
|
||||
# The value '20' equates to logging.INFO
|
||||
# It's not necessary to set this, but we will for
|
||||
# the sake of completeness
|
||||
self.setValue("Logging/Severity", "20")
|
||||
self.setValue("Logging/Logfile", str(Path.home() / 'LinkScope_logfile.log'))
|
||||
def getGroupSettings(self, settingsGroup: str) -> dict:
|
||||
if not settingsGroup.endswith('/'):
|
||||
settingsGroup += '/'
|
||||
settingsDict = {
|
||||
setting: self.globalSettings.value(setting)
|
||||
for setting in self.globalSettings.allKeys()
|
||||
if setting.startswith(settingsGroup)
|
||||
}
|
||||
for setting in self:
|
||||
if setting.startswith(settingsGroup):
|
||||
settingsDict[setting] = self[setting]
|
||||
return dict(sorted(settingsDict.items()))
|
||||
|
||||
# Usability Alias
|
||||
def setValue(self, key, value):
|
||||
def setValue(self, key, value) -> None:
|
||||
if self.globalSettings.contains(key):
|
||||
self.globalSettings.setValue(key, value)
|
||||
self[key] = value
|
||||
|
||||
def setGlobalValue(self, key, value) -> None:
|
||||
"""
|
||||
Helper in the case we want to be explicit in setting a value globally.
|
||||
"""
|
||||
self.globalSettings.setValue(key, value)
|
||||
|
||||
def value(self, key, alt=None):
|
||||
if self.globalSettings.contains(key):
|
||||
return self.globalSettings.value(key)
|
||||
return self.get(key, alt)
|
||||
|
||||
def save(self):
|
||||
def removeKey(self, key) -> bool:
|
||||
try:
|
||||
if self.globalSettings.contains(key):
|
||||
self.globalSettings.remove(key)
|
||||
else:
|
||||
self.pop(key)
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def save(self) -> None:
|
||||
# Save and then move to prevent corruption if the application closes unexpectedly.
|
||||
actualSavePath = str(Path(self.value("Project/BaseDir")).joinpath(self.value("Project/Name") + ".linkscope"))
|
||||
if is_path_exists_or_creatable_portable(actualSavePath):
|
||||
@@ -67,7 +139,12 @@ class SettingsObject(dict):
|
||||
with open(tempSavePath, "wb") as projectFile:
|
||||
dump(self, projectFile)
|
||||
move(tempSavePath, actualSavePath)
|
||||
self.globalSettings.sync()
|
||||
globalSettingsSavingError = self.globalSettings.status()
|
||||
if globalSettingsSavingError != self.globalSettings.Status.NoError:
|
||||
raise ValueError(f'Could not save global settings: {globalSettingsSavingError}')
|
||||
|
||||
def load(self, savedDict: dict):
|
||||
def load(self, savedDict: dict) -> None:
|
||||
# No need to do anything with global settings.
|
||||
for key in savedDict:
|
||||
self[key] = savedDict[key]
|
||||
|
||||
@@ -6,7 +6,6 @@ from pathlib import Path
|
||||
from os import symlink
|
||||
from shutil import copy2
|
||||
from hashlib import sha3_512
|
||||
from binascii import hexlify
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from PySide6 import QtCore
|
||||
@@ -55,18 +54,25 @@ class URLManager:
|
||||
if savePathString == 'None':
|
||||
return None
|
||||
|
||||
fileType = magic.from_file(urlPathString, mime=True)
|
||||
fileTypeSplit1, fileTypeSplit2 = fileType.split('/', 1)
|
||||
# CSV files not considered - may have any dialect, hard to accommodate.
|
||||
if urlPath.suffix in {'.ods', '.xls', '.xlsm', '.xlsx'} and \
|
||||
fileTypeSplit2 in {'vnd.oasis.opendocument.spreadsheet',
|
||||
'vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'vnd.ms-excel'}:
|
||||
return {"Spreadsheet Name": urlName,
|
||||
"File Path": savePathString,
|
||||
"Entity Type": "Spreadsheet"}
|
||||
# Only support zip files for archives (for now) 10/Jul/2021).
|
||||
if zipfile.is_zipfile(urlPathString):
|
||||
entityJson = {"Archive Name": urlName, "File Path": savePathString, "Entity Type": "Archive"}
|
||||
elif zipfile.is_zipfile(urlPathString):
|
||||
return {"Archive Name": urlName, "File Path": savePathString, "Entity Type": "Archive"}
|
||||
elif fileTypeSplit1 == "video":
|
||||
return {"Video Name": urlName, "File Path": savePathString, "Entity Type": "Video"}
|
||||
elif fileTypeSplit1 == "image":
|
||||
return {"Image Name": urlName, "File Path": savePathString, "Entity Type": "Image"}
|
||||
else:
|
||||
fileType = magic.from_file(urlPathString, mime=True).split('/')[0]
|
||||
if fileType == "video":
|
||||
entityJson = {"Video Name": urlName, "File Path": savePathString, "Entity Type": "Video"}
|
||||
elif fileType == "image":
|
||||
entityJson = {"Image Name": urlName, "File Path": savePathString, "Entity Type": "Image"}
|
||||
else:
|
||||
entityJson = {"Document Name": urlName, "File Path": savePathString, "Entity Type": "Document"}
|
||||
return entityJson
|
||||
return {"Document Name": urlName, "File Path": savePathString, "Entity Type": "Document"}
|
||||
|
||||
def moveURLToProjectFilesHelperIfNeeded(self, urlPath: Path):
|
||||
valuePath = Path(urlPath).absolute()
|
||||
@@ -83,8 +89,8 @@ class URLManager:
|
||||
|
||||
projectFilesPath = Path(self.mainWindow.SETTINGS.value("Project/FilesDir"))
|
||||
# Create a unique path in Project Files
|
||||
saveHash = hexlify(sha3_512(str(urlPath).encode()).digest()).decode()[:16] # nosec
|
||||
savePath = projectFilesPath / f'{saveHash}|{urlPath.name}'
|
||||
saveHash = sha3_512(str(urlPath).encode()).hexdigest()[:16] # nosec
|
||||
savePath = projectFilesPath / f'{saveHash}_{urlPath.name}'
|
||||
|
||||
if createSymlink:
|
||||
symlink(urlPath, savePath)
|
||||
|
||||
193
Core/UpdateManager.py
Normal file
193
Core/UpdateManager.py
Normal file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import contextlib
|
||||
import platform
|
||||
import subprocess
|
||||
import requests
|
||||
import tempfile
|
||||
import shutil
|
||||
import os
|
||||
import ctypes
|
||||
|
||||
from pathlib import Path
|
||||
from semver import compare
|
||||
from PySide6 import QtCore, QtWidgets
|
||||
|
||||
|
||||
class UpdateManager:
|
||||
|
||||
def __init__(self, mainWindow):
|
||||
self.updateThread = None
|
||||
self.mainWindow = mainWindow
|
||||
self.system = platform.system()
|
||||
if self.system == 'Windows':
|
||||
self.baseSoftwarePath = Path(os.path.abspath(os.sep)) / 'Program Files' / 'LinkScope'
|
||||
else:
|
||||
self.baseSoftwarePath = Path(os.path.abspath(os.sep)) / 'usr' / 'local' / 'sbin' / 'LinkScope'
|
||||
|
||||
def getDownloadURL(self):
|
||||
downloadURLBase = self.mainWindow.SETTINGS.value("Program/Update Source")
|
||||
if self.system == 'Windows':
|
||||
return f"{downloadURLBase}LinkScope-Windows-x64.7z"
|
||||
else:
|
||||
return f"{downloadURLBase}LinkScope-Ubuntu-x64.7z"
|
||||
|
||||
def getLatestVersion(self):
|
||||
with contextlib.suppress(Exception):
|
||||
version_req = requests.get(self.mainWindow.SETTINGS.value("Program/Version Check Source"),
|
||||
headers={'User-Agent': 'LinkScope Update Checker'})
|
||||
if version_req.status_code == 200:
|
||||
return version_req.json()['tag_name']
|
||||
return None
|
||||
|
||||
def isUpdateAvailable(self):
|
||||
latest_version = self.getLatestVersion()
|
||||
return (
|
||||
latest_version is not None
|
||||
and compare(self.mainWindow.SETTINGS.value("Program/Version").lstrip('v'),
|
||||
latest_version.lstrip('v')) < 0
|
||||
)
|
||||
|
||||
def doUpdate(self) -> None:
|
||||
if self.updateThread is not None:
|
||||
self.mainWindow.MESSAGEHANDLER.error('Update already in progress.')
|
||||
return
|
||||
self.mainWindow.MESSAGEHANDLER.info('Starting update...')
|
||||
self.updateThread = UpdaterThread(self, self.mainWindow)
|
||||
self.updateThread.updateDoneSignal.connect(self.finalizeUpdate)
|
||||
self.updateThread.start()
|
||||
self.mainWindow.MESSAGEHANDLER.info('Update in progress')
|
||||
|
||||
def finalizeUpdate(self, updateTempPath: str) -> None:
|
||||
if updateTempPath != '':
|
||||
latest_version = self.getLatestVersion()
|
||||
self.mainWindow.SETTINGS.setValue("Program/Version", latest_version)
|
||||
self.mainWindow.MESSAGEHANDLER.info('Updating done, please restart for the changes to take effect.')
|
||||
self.mainWindow.MESSAGEHANDLER.info("The application will now save and close to apply the updates. "
|
||||
"Please wait for a few minutes before reopening LinkScope.",
|
||||
popUp=True)
|
||||
|
||||
uncompressNewVersion(
|
||||
self.system,
|
||||
self.mainWindow.SETTINGS.value("Program/BaseDir"),
|
||||
str(self.baseSoftwarePath),
|
||||
updateTempPath)
|
||||
self.mainWindow.close()
|
||||
|
||||
else:
|
||||
self.mainWindow.MESSAGEHANDLER.info('Updating failed.')
|
||||
|
||||
|
||||
class UpdaterWindow(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, mainWindow, updateManager: UpdateManager, updateAvailableOverride: bool = False):
|
||||
super().__init__()
|
||||
self.mainWindow = mainWindow
|
||||
self.updateManager = updateManager
|
||||
|
||||
self.setWindowTitle('Update Manager')
|
||||
|
||||
layout = QtWidgets.QGridLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
doUpdateButton = QtWidgets.QPushButton('Update')
|
||||
doUpdateButton.clicked.connect(self.initiateUpdate)
|
||||
cancelButton = QtWidgets.QPushButton('Close')
|
||||
cancelButton.clicked.connect(self.reject)
|
||||
|
||||
updateAvailableLabel = QtWidgets.QLabel()
|
||||
updateAvailableLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
if updateAvailableOverride or updateManager.isUpdateAvailable():
|
||||
updateAvailableLabel.setText('An update for LinkScope is available.')
|
||||
else:
|
||||
updateAvailableLabel.setText('LinkScope is up to date.')
|
||||
doUpdateButton.setDisabled(True)
|
||||
doUpdateButton.setEnabled(False)
|
||||
|
||||
layout.addWidget(updateAvailableLabel, 1, 1, 1, 2)
|
||||
layout.addWidget(cancelButton, 2, 1)
|
||||
layout.addWidget(doUpdateButton, 2, 2)
|
||||
|
||||
def initiateUpdate(self) -> None:
|
||||
self.updateManager.doUpdate()
|
||||
self.accept()
|
||||
|
||||
|
||||
class UpdaterThread(QtCore.QThread):
|
||||
updateDoneSignal = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, updateManager, mainWindow):
|
||||
super().__init__()
|
||||
self.mainWindow = mainWindow
|
||||
self.updateManager = updateManager
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
downloadUrl = self.updateManager.getDownloadURL()
|
||||
clientTempCompressedArchive = tempfile.mkstemp(suffix='.7z')
|
||||
tempPath = clientTempCompressedArchive[1]
|
||||
|
||||
with os.fdopen(clientTempCompressedArchive[0], 'wb') as tempArchive:
|
||||
with requests.get(downloadUrl, stream=True) as fileStream:
|
||||
for chunk in fileStream.iter_content(chunk_size=5 * 1024 * 1024):
|
||||
tempArchive.write(chunk)
|
||||
self.updateDoneSignal.emit(tempPath)
|
||||
except Exception:
|
||||
self.updateDoneSignal.emit('')
|
||||
|
||||
|
||||
def uncompressNewVersion(system: str, baseDir: str, baseSoftwarePath: str, updateTempPath: str):
|
||||
tempDir = tempfile.mkdtemp(prefix='LinkScope_Updater_TMP_')
|
||||
|
||||
if system == 'Windows':
|
||||
updaterPath = Path(baseDir) / "UpdaterUtil.exe"
|
||||
|
||||
tempUpdaterPath = Path(tempDir) / updaterPath.name
|
||||
shutil.copy(updaterPath, tempUpdaterPath)
|
||||
|
||||
# This is done so that Windows spawns the updater as a detached process.
|
||||
ShellExecuteEx = ctypes.windll.shell32.ShellExecuteEx
|
||||
SEE_MASK_NO_CONSOLE = 0x00008000
|
||||
|
||||
class SHELLEXECUTEINFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cbSize", ctypes.c_ulong),
|
||||
("fMask", ctypes.c_ulong),
|
||||
("hwnd", ctypes.c_void_p),
|
||||
("lpVerb", ctypes.c_char_p),
|
||||
("lpFile", ctypes.c_char_p),
|
||||
("lpParameters", ctypes.c_char_p),
|
||||
("lpDirectory", ctypes.c_char_p),
|
||||
("nShow", ctypes.c_int),
|
||||
("hInstApp", ctypes.c_void_p),
|
||||
("lpIDList", ctypes.c_void_p),
|
||||
("lpClass", ctypes.c_char_p),
|
||||
("hkeyClass", ctypes.c_void_p),
|
||||
("dwHotKey", ctypes.c_ulong),
|
||||
("hIconOrMonitor", ctypes.c_void_p),
|
||||
("hProcess", ctypes.c_void_p),
|
||||
]
|
||||
|
||||
sei = SHELLEXECUTEINFO()
|
||||
sei.cbSize = ctypes.sizeof(sei)
|
||||
sei.fMask = SEE_MASK_NO_CONSOLE
|
||||
sei.lpVerb = b"runas"
|
||||
sei.lpFile = bytes(tempUpdaterPath)
|
||||
sei.lpParameters = f'"{updateTempPath}" "{baseSoftwarePath}"'.encode('utf-8')
|
||||
sei.nShow = 1
|
||||
|
||||
if not ShellExecuteEx(ctypes.byref(sei)):
|
||||
raise ctypes.WinError()
|
||||
|
||||
elif system == 'Linux':
|
||||
updaterPath = Path(baseDir) / "UpdaterUtil"
|
||||
|
||||
tempUpdaterPath = Path(tempDir) / updaterPath.name
|
||||
shutil.copy(updaterPath, tempUpdaterPath)
|
||||
|
||||
subprocess.Popen(
|
||||
f'pkexec "{tempUpdaterPath}" "{updateTempPath}" "{baseSoftwarePath}"',
|
||||
start_new_session=True,
|
||||
close_fds=True,
|
||||
shell=True,
|
||||
)
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import platform
|
||||
import subprocess
|
||||
import tempfile
|
||||
import stat
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
@@ -14,19 +15,17 @@ import py7zr
|
||||
from PySide6 import QtCore, QtWidgets, QtGui
|
||||
|
||||
# Requirements:
|
||||
# requests
|
||||
# PySide6
|
||||
# py7zr
|
||||
# requests PySide6 py7zr wheel pip nuitka ordered-set zstandard
|
||||
#
|
||||
# Compile with (requires zstandard, benefits from orderedset):
|
||||
# Compile with:
|
||||
#
|
||||
# Windows:
|
||||
"""
|
||||
python -m nuitka --follow-imports --onefile --noinclude-pytest-mode=nofollow --noinclude-setuptools-mode=nofollow ^
|
||||
--noinclude-custom-mode=setuptools:error --noinclude-IPython-mode=nofollow --enable-plugin=pyside6 ^
|
||||
--assume-yes-for-downloads --remove-output --disable-console --warn-unusual-code --show-modules ^
|
||||
--windows-company-name="AccentuSoft" --windows-product-name="LinkScope Installer" --windows-product-version=2.0.0.0 ^
|
||||
--include-data-files="Icon.ico=Icon.ico" --linux-icon="Icon.ico" --windows-icon-from-ico=".\Icon.ico" ^
|
||||
--assume-yes-for-downloads --remove-output --windows-console-mode=disable --warn-unusual-code --show-modules ^
|
||||
--windows-company-name="AccentuSoft" --windows-product-name="LinkScope Installer" --windows-product-version=1.6.5.0 ^
|
||||
--include-data-files="Icon.ico=Icon.ico" --windows-icon-from-ico=".\Icon.ico" ^
|
||||
--windows-file-description="LinkScope Installer" ^
|
||||
Installer.py
|
||||
"""
|
||||
@@ -35,7 +34,7 @@ Installer.py
|
||||
"""
|
||||
python -m nuitka --follow-imports --onefile --noinclude-pytest-mode=nofollow --noinclude-setuptools-mode=nofollow \
|
||||
--noinclude-custom-mode=setuptools:error --noinclude-IPython-mode=nofollow --enable-plugin=pyside6 \
|
||||
--assume-yes-for-downloads --remove-output --disable-console --warn-unusual-code --show-modules \
|
||||
--assume-yes-for-downloads --remove-output --warn-unusual-code --show-modules \
|
||||
--include-data-files="Icon.ico=Icon.ico" --linux-icon="Icon.ico" \
|
||||
Installer.py
|
||||
"""
|
||||
@@ -717,6 +716,117 @@ For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
"""
|
||||
|
||||
|
||||
def recursiveMakeWriteableHelper(targetPath: Path):
|
||||
for root, dirs, files in os.walk(targetPath):
|
||||
for dir_name in dirs:
|
||||
dir_path = os.path.join(root, dir_name)
|
||||
os.chmod(dir_path, stat.S_IWRITE)
|
||||
if not os.access(dir_path, os.W_OK):
|
||||
os.chmod(dir_path, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
|
||||
for file_name in files:
|
||||
file_path = os.path.join(root, file_name)
|
||||
os.chmod(file_path, stat.S_IWRITE)
|
||||
if not os.access(file_path, os.W_OK):
|
||||
os.chmod(file_path, stat.S_IWRITE | stat.S_IREAD)
|
||||
|
||||
def deleteVenvStuff() -> None:
|
||||
baseAppStoragePath = Path(
|
||||
QtCore.QStandardPaths.standardLocations(
|
||||
QtCore.QStandardPaths.StandardLocation.AppDataLocation)[0])
|
||||
recursiveMakeWriteableHelper(baseAppStoragePath)
|
||||
shutil.rmtree(baseAppStoragePath)
|
||||
|
||||
|
||||
def installGraphviz() -> None:
|
||||
if platform.system() == 'Linux':
|
||||
installGraphvizLinuxHelper()
|
||||
else:
|
||||
installGraphvizWindowsHelper()
|
||||
|
||||
|
||||
def installGraphvizLinuxHelper():
|
||||
subprocess.run(['apt', 'update'])
|
||||
# https://doc.qt.io/qt-6/linux-requirements.html
|
||||
# https://github.com/Nuitka/Nuitka/issues/2138
|
||||
# No need to check if this succeeds - if there are any issues with installation, we will throw
|
||||
# an error on the install command.
|
||||
command = subprocess.run(['apt', 'install', 'p7zip-full', 'libopengl0', 'graphviz', 'libmagic1',
|
||||
'libfontconfig1-dev', 'libfreetype6-dev', 'libatspi2.0-dev',
|
||||
'libcairo2-dev', 'python3-dev', 'pkg-config', '-y'])
|
||||
if command.returncode != 0:
|
||||
raise ValueError('Installing new packages failed, cannot continue installation.')
|
||||
|
||||
|
||||
def installGraphvizWindowsHelper():
|
||||
graphVizPage = requests.get('https://graphviz.org/download/')
|
||||
graphVizParts = graphVizPage.text.split('\n')
|
||||
graphVizDownloadLink = next((chunk.split('"')[1] for chunk in graphVizParts
|
||||
if '(64-bit) EXE installer' in chunk), "")
|
||||
|
||||
if not isinstance(graphVizDownloadLink, str) or graphVizDownloadLink == "":
|
||||
raise ValueError('Cannot install GraphViz: Failed to locate the latest version of the GraphViz installer.')
|
||||
graphVizInstallerFileHandler, graphVizInstallerTemp = tempfile.mkstemp()
|
||||
tempPath = Path(graphVizInstallerTemp)
|
||||
|
||||
with os.fdopen(graphVizInstallerFileHandler, 'wb') as tempInstallerFile:
|
||||
with requests.get(graphVizDownloadLink, stream=True) as fileStream:
|
||||
for chunk in fileStream.iter_content(chunk_size=5 * 1024 * 1024):
|
||||
tempInstallerFile.write(chunk)
|
||||
tempPath.chmod(tempPath.stat().st_mode | 0o111)
|
||||
subprocess.run([str(tempPath)])
|
||||
tempPath.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def installPythonLinuxHelper():
|
||||
subprocess.run('echo "y" | add-apt-repository ppa:deadsnakes/ppa', shell=True)
|
||||
subprocess.run('apt-get update && apt-get install python3.13 python3.13-venv -y', shell=True)
|
||||
subprocess.run('wget -O - https://bootstrap.pypa.io/get-pip.py | python3.13', shell=True)
|
||||
|
||||
|
||||
def installPythonWindowsHelper():
|
||||
win_downloads = requests.get('https://www.python.org/downloads/windows/')
|
||||
|
||||
if win_downloads.status_code != 200:
|
||||
return False
|
||||
|
||||
latest_release_path = win_downloads.text.split(
|
||||
'Python 3.13.', 1)[0].split('href="')[-1].split('"', 1)[0]
|
||||
|
||||
latest_release_full_path = f'https://www.python.org{latest_release_path}'
|
||||
|
||||
latest_release_page = requests.get(latest_release_full_path)
|
||||
|
||||
if latest_release_page.status_code != 200:
|
||||
return False
|
||||
|
||||
latest_release_installer_path = latest_release_page.text.split(
|
||||
'Windows installer (64-bit)', 1)[0].split('href="')[-1].split('"', 1)[0]
|
||||
|
||||
latest_release_binary = requests.get(latest_release_installer_path, allow_redirects=True)
|
||||
|
||||
if latest_release_binary.status_code != 200:
|
||||
return False
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmpdir_path = Path(tmpdir)
|
||||
installer_file_path = tmpdir_path / 'python.exe'
|
||||
installer_file_path.touch(mode=0o777)
|
||||
with open(installer_file_path, 'wb') as file:
|
||||
file.write(latest_release_binary.content)
|
||||
ctypes.windll.shell32.ShellExecuteW(None, "runas", installer_file_path, "/quiet", None, 1)
|
||||
|
||||
|
||||
def removeFileHelper(pathToRemove: Path):
|
||||
if not pathToRemove.exists():
|
||||
return
|
||||
if pathToRemove.is_dir():
|
||||
recursiveMakeWriteableHelper(pathToRemove)
|
||||
shutil.rmtree(pathToRemove)
|
||||
else:
|
||||
os.chmod(pathToRemove, stat.S_IWRITE)
|
||||
pathToRemove.unlink(missing_ok=True)
|
||||
|
||||
|
||||
class InstallWizard(QtWidgets.QWizard):
|
||||
|
||||
# INSTALLER PATHS:
|
||||
@@ -727,19 +837,10 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
# 0 -> 1 -> 4 -> 2 -> 5 -> 6 -> -1
|
||||
# Install, Graphviz exists
|
||||
# 0 -> 1 -> 4 -> 5 -> 6 -> -1
|
||||
# Update
|
||||
# 0 -> 4 -> 2 -> 5 -> 6 -> -1
|
||||
# -> 5 -> 6 -> -1
|
||||
|
||||
def nextId(self) -> int:
|
||||
if self.currentId() == 0:
|
||||
if self.currentPage().uninstallRadio.isChecked():
|
||||
return 3
|
||||
elif self.currentPage().updateRadio.isChecked():
|
||||
return 4
|
||||
else:
|
||||
# Default action is install.
|
||||
return 1
|
||||
# Default action is install.
|
||||
return 3 if self.currentPage().uninstallRadio.isChecked() else 1
|
||||
if self.currentId() == 1:
|
||||
return 4
|
||||
if self.currentId() == 2:
|
||||
@@ -763,10 +864,7 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
self.trayIcon.show()
|
||||
|
||||
if len(sys.argv) < 5:
|
||||
latestReleaseDetails = requests.get('https://github.com/AccentuSoft/LinkScope_Client/releases/latest',
|
||||
headers={'Accept': 'application/json'})
|
||||
latestVersion = latestReleaseDetails.json()['tag_name']
|
||||
downloadURLBase = f"https://github.com/AccentuSoft/LinkScope_Client/releases/download/{latestVersion}/"
|
||||
downloadURLBase = "https://github.com/AccentuSoft/LinkScope_Client/releases/latest/download/"
|
||||
|
||||
if self.currentOS == 'Windows':
|
||||
try:
|
||||
@@ -783,11 +881,15 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
self.graphvizExists = graphvizPath.exists()
|
||||
self.baseSoftwarePath = Path(os.path.abspath(os.sep)) / 'Program Files' / 'LinkScope'
|
||||
self.executablePath = self.baseSoftwarePath / 'LinkScope.exe'
|
||||
self.downloadURL = downloadURLBase + "LinkScope-Windows-x64.7z"
|
||||
self.downloadURL = f"{downloadURLBase}LinkScope-Windows-x64.7z"
|
||||
|
||||
newArgs = ['"' + str(self.desktopShortcutPath) + '"', str(self.graphvizExists),
|
||||
'"' + str(self.baseSoftwarePath) + '"', '"' + str(self.executablePath) + '"',
|
||||
'"' + self.downloadURL + '"']
|
||||
newArgs = [
|
||||
f'"{str(self.desktopShortcutPath)}"',
|
||||
str(self.graphvizExists),
|
||||
f'"{str(self.baseSoftwarePath)}"',
|
||||
f'"{str(self.executablePath)}"',
|
||||
f'"{self.downloadURL}"',
|
||||
]
|
||||
ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, " ".join(newArgs), None, 1)
|
||||
sys.exit(0)
|
||||
elif self.currentOS == 'Linux':
|
||||
@@ -804,7 +906,7 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
self.appPath = Path(
|
||||
os.path.abspath(os.sep)) / 'usr' / 'share' / 'applications' / 'LinkScope.desktop'
|
||||
self.executablePath = self.baseSoftwarePath / 'LinkScope'
|
||||
self.downloadURL = downloadURLBase + "LinkScope-Ubuntu-x64.7z"
|
||||
self.downloadURL = f"{downloadURLBase}LinkScope-Ubuntu-x64.7z"
|
||||
|
||||
# No need to wrap these in quotes
|
||||
newArgs = [str(self.desktopShortcutPath), str(self.graphvizExists), str(self.baseSoftwarePath),
|
||||
@@ -815,7 +917,7 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
sudoPassword = QtWidgets.QInputDialog.getText(self, 'Sudo Password',
|
||||
'Installation requires elevated privileges. '
|
||||
'Please enter your password: ',
|
||||
QtWidgets.QLineEdit.Password)
|
||||
QtWidgets.QLineEdit.EchoMode.Password)
|
||||
if sudoPassword[1] and sudoPassword[0] != '':
|
||||
sudoPrivs = subprocess.Popen(['sudo', '-S', '-H', '-k', sys.executable, *newArgs],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
@@ -865,7 +967,7 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
sys.exit(-2)
|
||||
self.appPath = Path(sys.argv[6])
|
||||
|
||||
self.setWizardStyle(self.ModernStyle)
|
||||
self.setWizardStyle(self.WizardStyle.ModernStyle)
|
||||
self.setWindowTitle('LinkScope Installer')
|
||||
|
||||
# Normally one would use enums to keep track of pages, but the installer crashes if we try, so we
|
||||
@@ -888,38 +990,23 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
self.addPage(licensePage)
|
||||
self.addPage(installUpgradePage)
|
||||
self.addPage(DonePage())
|
||||
self.setOptions(self.NoBackButtonOnStartPage | self.NoBackButtonOnLastPage | self.CancelButtonOnLeft |
|
||||
self.NoCancelButtonOnLastPage)
|
||||
self.setOptions(self.WizardOption.NoBackButtonOnStartPage | self.WizardOption.NoBackButtonOnLastPage |
|
||||
self.WizardOption.CancelButtonOnLeft | self.WizardOption.NoCancelButtonOnLastPage)
|
||||
|
||||
self.show()
|
||||
|
||||
def removeFileHelper(self, pathToRemove: Path):
|
||||
if not pathToRemove.exists():
|
||||
return
|
||||
pathToRemove.chmod(0o777)
|
||||
if pathToRemove.is_dir():
|
||||
for dirpath, dirnames, filenames in os.walk(pathToRemove):
|
||||
Path(dirpath).chmod(0o777)
|
||||
filenames.extend(dirnames)
|
||||
for filename in filenames:
|
||||
filePath = Path(dirpath) / filename
|
||||
filePath.chmod(0o777)
|
||||
shutil.rmtree(pathToRemove)
|
||||
else:
|
||||
pathToRemove.unlink(missing_ok=True)
|
||||
|
||||
def createShortcut(self):
|
||||
self.removeFileHelper(self.desktopShortcutPath)
|
||||
removeFileHelper(self.desktopShortcutPath)
|
||||
if self.currentOS == 'Linux':
|
||||
QtCore.QFile.link(str(self.appPath), str(self.desktopShortcutPath))
|
||||
elif self.currentOS == 'Windows':
|
||||
QtCore.QFile.link(str(self.executablePath), str(self.desktopShortcutPath))
|
||||
|
||||
def uninstall(self):
|
||||
self.removeFileHelper(self.desktopShortcutPath)
|
||||
removeFileHelper(self.desktopShortcutPath)
|
||||
if self.currentOS == 'Linux':
|
||||
self.removeFileHelper(self.appPath)
|
||||
self.removeFileHelper(self.baseSoftwarePath)
|
||||
removeFileHelper(self.appPath)
|
||||
removeFileHelper(self.baseSoftwarePath)
|
||||
|
||||
def downloadClient(self):
|
||||
if not isinstance(self.downloadURL, str) or self.downloadURL == "":
|
||||
@@ -937,58 +1024,39 @@ class InstallWizard(QtWidgets.QWizard):
|
||||
|
||||
tempPath.unlink(missing_ok=True)
|
||||
|
||||
def downloadGraphviz(self):
|
||||
graphVizPage = requests.get('https://graphviz.org/download/')
|
||||
graphVizParts = graphVizPage.text.split('\n')
|
||||
graphVizDownloadLink = next((chunk.split('"')[1] for chunk in graphVizParts
|
||||
if '(64-bit) EXE installer' in chunk), "")
|
||||
|
||||
if not isinstance(graphVizDownloadLink, str) or graphVizDownloadLink == "":
|
||||
raise ValueError('Cannot install GraphViz: Failed to locate the latest version of the GraphViz installer.')
|
||||
graphVizInstallerTemp = tempfile.mkstemp()
|
||||
tempPath = Path(graphVizInstallerTemp[1])
|
||||
|
||||
with os.fdopen(graphVizInstallerTemp[0], 'wb') as tempInstallerFile:
|
||||
with requests.get(graphVizDownloadLink, stream=True) as fileStream:
|
||||
for chunk in fileStream.iter_content(chunk_size=5 * 1024 * 1024):
|
||||
tempInstallerFile.write(chunk)
|
||||
tempPath.chmod(tempPath.stat().st_mode | 0o111)
|
||||
subprocess.run([str(tempPath)])
|
||||
tempPath.unlink(missing_ok=True)
|
||||
|
||||
def install(self):
|
||||
# Assumes we have superuser privileges.
|
||||
if self.currentOS != 'Linux':
|
||||
# Assume the user has installed / will install Graphviz.
|
||||
# We don't actually need to do anything here. Maybe in the future, register application in registry?
|
||||
return
|
||||
# Redundant since we always try to update and install, but it's good coding practice.
|
||||
if not self.graphvizExists:
|
||||
# No need to check if this succeeds - if there are any issues with installation, we will throw
|
||||
# an error on the install command.
|
||||
subprocess.run(['apt', 'update'])
|
||||
command = subprocess.run(['apt', 'install', 'p7zip-full', 'libopengl0', 'graphviz', 'libmagic1', '-y'])
|
||||
if command.returncode != 0:
|
||||
raise ValueError('Installing new packages failed, cannot continue installation.')
|
||||
# We do not install/configure anything on the virtual env, the first time boot will take longer, but
|
||||
# it's safer to do it like this since we don't know how the user's system is configured.
|
||||
|
||||
self.removeFileHelper(self.appPath)
|
||||
with open(self.appPath, 'w') as desktopApplicationFile:
|
||||
desktopApplicationFile.write(LINUX_DESKTOP_FILE_ENTRY)
|
||||
# Mark desktop file as executable
|
||||
self.appPath.chmod(self.appPath.stat().st_mode | 0o111)
|
||||
if self.currentOS == 'Linux':
|
||||
try:
|
||||
if subprocess.run(["python3.13", "--version"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True).returncode != 0:
|
||||
raise ValueError()
|
||||
except Exception:
|
||||
installPythonLinuxHelper()
|
||||
elif not subprocess.check_output(['where', 'python']):
|
||||
installPythonWindowsHelper()
|
||||
|
||||
if not self.graphvizExists or self.currentOS == 'Linux':
|
||||
installGraphviz()
|
||||
|
||||
if self.currentOS == 'Linux':
|
||||
removeFileHelper(self.appPath)
|
||||
with open(self.appPath, 'w') as desktopApplicationFile:
|
||||
desktopApplicationFile.write(LINUX_DESKTOP_FILE_ENTRY)
|
||||
# Mark desktop file as executable
|
||||
self.appPath.chmod(self.appPath.stat().st_mode | 0o111)
|
||||
|
||||
|
||||
class IntroInstallUninstallPage(QtWidgets.QWizardPage):
|
||||
|
||||
def validatePage(self) -> bool:
|
||||
if self.updateRadio.isChecked():
|
||||
self.wizard().page(5).setTitle('Updating LinkScope Installation')
|
||||
self.wizard().page(5).installProgressLabel.setText('Update Process: ')
|
||||
self.wizard().page(5).updateSelected = True
|
||||
else:
|
||||
self.wizard().page(5).setTitle('Install LinkScope')
|
||||
self.wizard().page(5).installProgressLabel.setText('Installation Process: ')
|
||||
self.wizard().page(5).updateSelected = False
|
||||
self.wizard().page(5).setTitle('Install LinkScope')
|
||||
self.wizard().page(5).installProgressLabel.setText('Installation Process: ')
|
||||
return True
|
||||
|
||||
def __init__(self):
|
||||
@@ -1000,17 +1068,13 @@ class IntroInstallUninstallPage(QtWidgets.QWizardPage):
|
||||
self.setLayout(installUninstallLayout)
|
||||
|
||||
actionLabel = QtWidgets.QLabel("Please select the action that you wish to carry out:")
|
||||
actionLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
actionLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
installUninstallLayout.addWidget(actionLabel)
|
||||
|
||||
self.installRadio = QtWidgets.QRadioButton('Install LinkScope')
|
||||
self.installRadio.setToolTip('Install the LinkScope Client software.')
|
||||
self.installRadio.setChecked(True)
|
||||
|
||||
self.updateRadio = QtWidgets.QRadioButton('Update / Repair LinkScope')
|
||||
self.updateRadio.setToolTip('Update the existing installation of LinkScope Client, or repair any issues with '
|
||||
'the existing installation.')
|
||||
|
||||
self.uninstallRadio = QtWidgets.QRadioButton('Uninstall LinkScope')
|
||||
self.uninstallRadio.setToolTip('Uninstall the LinkScope Client software.\nNote that on Linux, any packages '
|
||||
'that were installed during the installation of LinkScope will not be removed.\n'
|
||||
@@ -1018,7 +1082,6 @@ class IntroInstallUninstallPage(QtWidgets.QWizardPage):
|
||||
'on them.')
|
||||
|
||||
installUninstallLayout.addWidget(self.installRadio)
|
||||
installUninstallLayout.addWidget(self.updateRadio)
|
||||
installUninstallLayout.addWidget(self.uninstallRadio)
|
||||
|
||||
|
||||
@@ -1034,7 +1097,7 @@ class WindowsGraphVizPage(QtWidgets.QWizardPage):
|
||||
'function, and will be installed along with the software. The licensing terms '
|
||||
'for GraphViz can be found at: https://graphviz.org/license/.')
|
||||
graphVizLabel.setWordWrap(True)
|
||||
graphVizLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
graphVizLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.setLayout(graphVizLayout)
|
||||
graphVizLayout.addWidget(graphVizLabel)
|
||||
|
||||
@@ -1077,12 +1140,11 @@ class LinkScopeInstallLatestPage(QtWidgets.QWizardPage):
|
||||
'LinkScope. Click "Commit" to start the installation.')
|
||||
|
||||
self.createShortcut = False
|
||||
self.updateSelected = False
|
||||
self.processStarted = False
|
||||
self.installThread = None
|
||||
|
||||
self.installLabel.setWordWrap(True)
|
||||
self.installLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.installLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
installLayout.addWidget(self.installLabel)
|
||||
|
||||
self.installProgressWidget = QtWidgets.QWidget()
|
||||
@@ -1097,7 +1159,7 @@ class LinkScopeInstallLatestPage(QtWidgets.QWizardPage):
|
||||
|
||||
self.downloadingLabel = QtWidgets.QLabel('Downloading files. This may take some time...')
|
||||
self.downloadingLabel.setWordWrap(True)
|
||||
self.downloadingLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.downloadingLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.downloadingLabel.setVisible(False)
|
||||
|
||||
installLayout.addWidget(self.installProgressWidget)
|
||||
@@ -1109,8 +1171,9 @@ class LinkScopeInstallLatestPage(QtWidgets.QWizardPage):
|
||||
class LinkScopeUninstallPage(QtWidgets.QWizardPage):
|
||||
|
||||
def doStuff(self):
|
||||
self.progressBar.setValue(3)
|
||||
try:
|
||||
deleteVenvStuff()
|
||||
self.progressBar.setValue(3)
|
||||
self.wizard().uninstall()
|
||||
except Exception as e:
|
||||
self.wizard().page(6).doneLabel.setText('Error occurred during uninstallation: ' +
|
||||
@@ -1135,7 +1198,7 @@ class LinkScopeUninstallPage(QtWidgets.QWizardPage):
|
||||
self.uninstallLabel = QtWidgets.QLabel('The installer will now uninstall LinkScope from this computer. Click '
|
||||
'"Commit" to begin the removal process.')
|
||||
self.uninstallLabel.setWordWrap(True)
|
||||
self.uninstallLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.uninstallLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
uninstallLayout.addWidget(self.uninstallLabel)
|
||||
|
||||
self.uninstallProgressWidget = QtWidgets.QWidget()
|
||||
@@ -1183,7 +1246,7 @@ class CreateDesktopShortcutPage(QtWidgets.QWizardPage):
|
||||
desktopShortcutLayout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(desktopShortcutLayout)
|
||||
desktopShortcutLabel = QtWidgets.QLabel('Create a Shortcut for LinkScope on the Desktop?')
|
||||
desktopShortcutLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
desktopShortcutLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.shortcutCheckbox = QtWidgets.QCheckBox('Create a Desktop Shortcut?')
|
||||
self.shortcutCheckbox.setChecked(True)
|
||||
@@ -1202,7 +1265,7 @@ class LicensePage(QtWidgets.QWizardPage):
|
||||
self.setLayout(licenseLayout)
|
||||
|
||||
licenseLabel = QtWidgets.QLabel('Please review carefully the license terms for the LinkScope Client software.')
|
||||
licenseLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
licenseLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
licenseLabel.setWordWrap(True)
|
||||
|
||||
licenseText = QtWidgets.QPlainTextEdit(AGPL_LICENSE)
|
||||
@@ -1215,8 +1278,6 @@ class LicensePage(QtWidgets.QWizardPage):
|
||||
rejectLicense.setChecked(True)
|
||||
|
||||
self.registerField('Accept Terms*', self.acceptLicense)
|
||||
self.setMinimumWidth(550)
|
||||
self.setMinimumHeight(600)
|
||||
|
||||
licenseLayout.addWidget(licenseLabel)
|
||||
licenseLayout.addWidget(licenseText)
|
||||
@@ -1235,7 +1296,7 @@ class DonePage(QtWidgets.QWizardPage):
|
||||
self.setLayout(doneLayout)
|
||||
self.doneLabel = QtWidgets.QLabel('Thank you for using LinkScope!\nClick "Finish" to exit the installer.')
|
||||
self.doneLabel.setWordWrap(True)
|
||||
self.doneLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.doneLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
doneLayout.addWidget(self.doneLabel)
|
||||
|
||||
|
||||
@@ -1250,22 +1311,15 @@ class InstallThread(QtCore.QThread):
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
# We have already installed Graphviz on Linux at this point.
|
||||
if not self.wizard.graphvizExists and self.wizard.currentOS == 'Windows':
|
||||
self.progressSignal.emit(1)
|
||||
self.wizard.downloadGraphviz()
|
||||
self.progressSignal.emit(2)
|
||||
if self.installPage.updateSelected:
|
||||
self.wizard.uninstall()
|
||||
self.wizard.install()
|
||||
self.installPage.downloadingLabel.setVisible(True)
|
||||
self.installPage.downloadingLabel.setHidden(False)
|
||||
self.progressSignal.emit(4)
|
||||
self.wizard.downloadClient()
|
||||
self.progressSignal.emit(8)
|
||||
shortcutExists = self.wizard.desktopShortcutPath.exists()
|
||||
self.progressSignal.emit(9)
|
||||
if self.installPage.createShortcut or (self.installPage.updateSelected and shortcutExists):
|
||||
if self.installPage.createShortcut:
|
||||
self.wizard.createShortcut()
|
||||
self.progressSignal.emit(10)
|
||||
self.doneSignal.emit(True)
|
||||
@@ -1278,5 +1332,7 @@ class InstallThread(QtCore.QThread):
|
||||
|
||||
if __name__ == '__main__':
|
||||
application = QtWidgets.QApplication(sys.argv)
|
||||
application.setOrganizationName("AccentuSoft")
|
||||
application.setApplicationName("LinkScope Client")
|
||||
installWizard = InstallWizard()
|
||||
sys.exit(application.exec())
|
||||
|
||||
2446
LinkScope.py
2446
LinkScope.py
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class AircraftInquiryByDealer:
|
||||
name = "Aircraft Inquiry By Dealer"
|
||||
category = "Aircraft"
|
||||
description = "Find information about aircraft identifications from https://registry.faa.gov/aircraftinquiry/"
|
||||
originTypes = {"Company"}
|
||||
resultTypes = {'Phrase', 'Company'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import pandas as pd
|
||||
from requests_futures.sessions import FuturesSession
|
||||
from concurrent.futures import as_completed
|
||||
|
||||
futures = []
|
||||
uidList = []
|
||||
return_result = []
|
||||
|
||||
submit_url = "https://registry.faa.gov/aircraftinquiry/Search/"
|
||||
crafted_url = f"{submit_url}DealerResult"
|
||||
with FuturesSession(max_workers=15) as session:
|
||||
for entity in entityJsonList:
|
||||
uidList.append(entity['uid'])
|
||||
futures.append(session.post(crafted_url, data={"Dealertxt": entity['Company Name']}))
|
||||
for future in as_completed(futures):
|
||||
uid = uidList[futures.index(future)]
|
||||
try:
|
||||
df_list = pd.read_html(future.result().text)
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
except ValueError:
|
||||
return_result = []
|
||||
return return_result
|
||||
df = df_list[0]
|
||||
for certificate_index in range(len(df["Certificate Number"])):
|
||||
index_of_child = len(return_result)
|
||||
return_result.append([{'Company Name': df["Name"][certificate_index],
|
||||
'Entity Type': 'Company'},
|
||||
{uid: {'Resolution': 'Aircraft Dealer', 'Notes': ''}}])
|
||||
return_result.append([{'Phrase': df["Certificate Number"][certificate_index],
|
||||
'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'Aircraft Certificate Number', 'Notes': ''}}])
|
||||
return return_result
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class AircraftInquiryByEngine:
|
||||
name = "Aircraft Inquiry By Engine"
|
||||
category = "Aircraft"
|
||||
description = "Find information about aircraft identifications from https://registry.faa.gov/aircraftinquiry/"
|
||||
originTypes = {"Phrase"}
|
||||
resultTypes = {"Phrase"}
|
||||
parameters = {'Manufacturer': {'description': "Enter the Manufacturer of the Engine Model",
|
||||
'type': 'String',
|
||||
'value': 'None'}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
from requests_futures.sessions import FuturesSession
|
||||
from concurrent.futures import as_completed
|
||||
import pandas as pd
|
||||
|
||||
Manufacturer = parameters['Manufacturer']
|
||||
|
||||
futures = []
|
||||
uidList = []
|
||||
return_result = []
|
||||
|
||||
submit_url = "https://registry.faa.gov/aircraftinquiry/Search/"
|
||||
crafted_url = f"{submit_url}EngineReferenceResult"
|
||||
with FuturesSession(max_workers=15) as session:
|
||||
for entity in entityJsonList:
|
||||
uidList.append(entity['uid'])
|
||||
futures.append(session.post(crafted_url, data={"Modeltxt": entity['Phrase'],
|
||||
"MfrNametxt": Manufacturer}))
|
||||
for future in as_completed(futures):
|
||||
uid = uidList[futures.index(future)]
|
||||
try:
|
||||
df_list = pd.read_html(future.result().text)
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
except ValueError:
|
||||
return "No results retrieved"
|
||||
df = df_list[0]
|
||||
return_result.append([{'Phrase': f"Model Code:str({df['Mfr/Mdl Code']})",
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': 'Aircraft Model Code', 'Notes': ''}}])
|
||||
return_result.append([{'Phrase': f"Engine Type:{df['Type Engine']}",
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': 'Aircraft Engine Type', 'Notes': ''}}])
|
||||
return_result.append([{'Phrase': f"Horse Power:str({df['Horsepower']})",
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': 'Aircraft Engine Horsepower', 'Notes': ''}}])
|
||||
return return_result
|
||||
@@ -1,60 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class AircraftInquiryByNNumber:
|
||||
name = "Aircraft Inquiry By N-Number"
|
||||
category = "Aircraft"
|
||||
description = "Find information about aircraft identifications from https://registry.faa.gov/aircraftinquiry/"
|
||||
originTypes = {"Phrase"}
|
||||
resultTypes = {'Phrase', 'Person', 'Identification Number', 'Company', 'Country', 'City'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
from requests_futures.sessions import FuturesSession
|
||||
from concurrent.futures import as_completed
|
||||
import pandas as pd
|
||||
|
||||
futures = []
|
||||
uidList = []
|
||||
return_result = []
|
||||
|
||||
submit_url = "https://registry.faa.gov/aircraftinquiry/Search/"
|
||||
crafted_url = f"{submit_url}NNumberResult"
|
||||
with FuturesSession(max_workers=15) as session:
|
||||
for entity in entityJsonList:
|
||||
uidList.append(entity['uid'])
|
||||
futures.append(session.post(crafted_url, data={"NNumbertxt": entity['Phrase']}))
|
||||
for future in as_completed(futures):
|
||||
uid = uidList[futures.index(future)]
|
||||
try:
|
||||
df_list = pd.read_html(future.result().text)
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
except ValueError:
|
||||
return "No results retrieved"
|
||||
df1 = df_list[0]
|
||||
df2 = df_list[1]
|
||||
df3 = df_list[2]
|
||||
return_result.append([{'ID Number': df1[1][0],
|
||||
'Entity Type': 'Identification Number'},
|
||||
{uid: {'Resolution': 'Aircraft Identification Number', 'Notes': ''}}])
|
||||
return_result.append([{'Company Name': df1[1][1],
|
||||
'Entity Type': 'Company'},
|
||||
{uid: {'Resolution': 'Aircraft Company', 'Notes': ''}}])
|
||||
return_result.append([{'Full Name': df2[1][0],
|
||||
'Entity Type': 'Person'},
|
||||
{uid: {'Resolution': 'Aircraft Owner', 'Notes': ''}}])
|
||||
return_result.append([{'City Name': df2[1][2],
|
||||
'Entity Type': 'City'},
|
||||
{uid: {'Resolution': "Aircraft Owner's City", 'Notes': ''}}])
|
||||
return_result.append([{'Country Name': df2[1][4],
|
||||
'Entity Type': 'Country'},
|
||||
{uid: {'Resolution': "Aircraft Owner's Country", 'Notes': ''}}])
|
||||
return_result.append([{'Phrase': df3[1][1],
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': "Aircraft Engine Series", 'Notes': ''}}])
|
||||
return_result.append([{'Phrase': df3[1][2],
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': "Aircraft Engine Motor", 'Notes': ''}}])
|
||||
return return_result
|
||||
@@ -1,47 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class AircraftInquiryByPersonName:
|
||||
name = "Aircraft Inquiry By Person Name"
|
||||
category = "Aircraft"
|
||||
description = "Find information about aircraft identifications from https://registry.faa.gov/aircraftinquiry/"
|
||||
originTypes = {"Person"}
|
||||
resultTypes = {'Phrase', 'Person', 'Identification Number', 'Company'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
from requests_futures.sessions import FuturesSession
|
||||
from concurrent.futures import as_completed
|
||||
import pandas as pd
|
||||
|
||||
futures = []
|
||||
uidList = []
|
||||
return_result = []
|
||||
|
||||
submit_url = "https://registry.faa.gov/aircraftinquiry/Search/"
|
||||
crafted_url = f"{submit_url}NameResult"
|
||||
with FuturesSession(max_workers=15) as session:
|
||||
for entity in entityJsonList:
|
||||
uidList.append(entity['uid'])
|
||||
futures.append(session.post(crafted_url, data={"nametxt": entity['Full Name'], "sort_option": "1"}))
|
||||
for future in as_completed(futures):
|
||||
uid = uidList[futures.index(future)]
|
||||
try:
|
||||
df_list = pd.read_html(future.result().text)
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
except ValueError:
|
||||
return "No results retrieved"
|
||||
df = df_list[0]
|
||||
for i in range(len(df["N-Number"])):
|
||||
return_result.append([{'Phrase': df["N-Number"][0],
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': 'Aircraft N-Number', 'Notes': ''}}])
|
||||
return_result.append([{'ID Number': str(df['Serial Number'][0]),
|
||||
'Entity Type': 'Identification Number'},
|
||||
{uid: {'Resolution': 'Aircraft Identification Number', 'Notes': ''}}])
|
||||
return_result.append([{'Company Name': df['Manufacturer Name Model'][0],
|
||||
'Entity Type': 'Company'},
|
||||
{uid: {'Resolution': 'Aircraft Manufacturer Name', 'Notes': ''}}])
|
||||
return return_result
|
||||
@@ -1,47 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class AircraftInquiryBySerialNumber:
|
||||
name = "Aircraft Inquiry By Serial Number"
|
||||
category = "Aircraft"
|
||||
description = "Find information about aircraft identifications from https://registry.faa.gov/aircraftinquiry/"
|
||||
originTypes = {"Identification Number"}
|
||||
resultTypes = {'Phrase', 'Person', 'Identification Number', 'Company'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
from requests_futures.sessions import FuturesSession
|
||||
from concurrent.futures import as_completed
|
||||
import pandas as pd
|
||||
|
||||
futures = []
|
||||
uidList = []
|
||||
return_result = []
|
||||
|
||||
submit_url = "https://registry.faa.gov/aircraftinquiry/Search/"
|
||||
crafted_url = f"{submit_url}SerialResult"
|
||||
with FuturesSession(max_workers=15) as session:
|
||||
for entity in entityJsonList:
|
||||
uidList.append(entity['uid'])
|
||||
futures.append(session.post(crafted_url, data={"Serialtxt": entity['ID Number'], "sort_option": "1"}))
|
||||
for future in as_completed(futures):
|
||||
uid = uidList[futures.index(future)]
|
||||
try:
|
||||
df_list = pd.read_html(future.result().text)
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
except ValueError:
|
||||
return "No results retrieved"
|
||||
df = df_list[0]
|
||||
for i in range(len(df["N-Number"])):
|
||||
return_result.append([{'Company Name': df["Manufacturer Name"][i],
|
||||
'Entity Type': 'Company'},
|
||||
{uid: {'Resolution': 'Aircraft Manufacturer', 'Notes': ''}}])
|
||||
return_result.append([{'Phrase': df["N-Number"][i],
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': 'Aircraft N-Number', 'Notes': ''}}])
|
||||
return_result.append([{'Phrase': df["Model"][i],
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': 'Aircraft Model', 'Notes': ''}}])
|
||||
return return_result
|
||||
@@ -1,3 +0,0 @@
|
||||
requests
|
||||
pandas
|
||||
requests-futures
|
||||
@@ -1,18 +0,0 @@
|
||||
<Aleph>
|
||||
<Aleph_ID>
|
||||
<Attributes>
|
||||
<Attribute default="Aleph Default ID" check="String" primary="True">ID</Attribute>
|
||||
</Attributes>
|
||||
<Icon>
|
||||
Default.svg
|
||||
</Icon>
|
||||
</Aleph_ID>
|
||||
<Aleph_Collection_ID>
|
||||
<Attributes>
|
||||
<Attribute default="Aleph Default Collection ID" check="String" primary="True">ID</Attribute>
|
||||
</Attributes>
|
||||
<Icon>
|
||||
Default.svg
|
||||
</Icon>
|
||||
</Aleph_Collection_ID>
|
||||
</Aleph>
|
||||
@@ -1,273 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class Aleph_Entity_Search:
|
||||
name = "Aleph Entity Search"
|
||||
category = "Aleph OCCRP"
|
||||
description = "Find information about a given search parameter"
|
||||
originTypes = {'Phrase', 'Person', 'Politically Exposed Person'}
|
||||
resultTypes = {'Phrase'}
|
||||
parameters = {'Max Results': {'description': 'The maximum number of results to return.',
|
||||
'type': 'String',
|
||||
'value': 'Enter the number of results you want returned',
|
||||
'default': '1'},
|
||||
'Aleph Disclaimer': {'description': 'The content on Aleph is provided for general information only.\n'
|
||||
'It is not intended to amount to advice on which you should place'
|
||||
'sole and entire reliance.\n'
|
||||
'We recommend that you conduct your own independent fact checking'
|
||||
'against the data and materials that you access on Aleph.\n'
|
||||
'Aleph API is not a replacement for traditional due diligence '
|
||||
'checks and know-your-customer background checks.',
|
||||
'type': 'String',
|
||||
'value': 'Type "Accept" (without quotes) to confirm your understanding.',
|
||||
'global': True}
|
||||
}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import pycountry
|
||||
import time
|
||||
from requests_futures.sessions import FuturesSession
|
||||
from concurrent.futures import as_completed
|
||||
|
||||
return_result = []
|
||||
uidList = []
|
||||
futures = []
|
||||
|
||||
url = "https://aleph.occrp.org/api/2/entities"
|
||||
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
|
||||
gender = "None"
|
||||
|
||||
if parameters['Aleph Disclaimer'] != 'Accept':
|
||||
return "Please Accept the Terms for Aleph."
|
||||
|
||||
try:
|
||||
max_results = int(parameters['Max Results'])
|
||||
except ValueError:
|
||||
return "The value for parameter 'Max Results' is not a valid integer."
|
||||
|
||||
if max_results <= 0:
|
||||
return []
|
||||
|
||||
with FuturesSession(max_workers=15) as session:
|
||||
for entity in entityJsonList:
|
||||
uidList.append(entity['uid'])
|
||||
primary_field = entity[list(entity)[1]].strip()
|
||||
crafted_url = url + f"?q={primary_field}&filter:schemata=Thing&limit={max_results}"
|
||||
time.sleep(1)
|
||||
futures.append(session.get(crafted_url, headers=headers))
|
||||
for future in as_completed(futures):
|
||||
uid = uidList[futures.index(future)]
|
||||
try:
|
||||
response = future.result().json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
# print(response)
|
||||
for schema in response['results']:
|
||||
index_of_child = len(return_result)
|
||||
try:
|
||||
if schema['schema'] == "Person":
|
||||
if schema['properties'].get('gender') is not None \
|
||||
and schema['properties'].get('gender')[0] == "F":
|
||||
gender = "Female"
|
||||
elif schema['properties'].get('gender') is not None \
|
||||
and schema['properties'].get('gender')[0] == "M":
|
||||
gender = "Male"
|
||||
if schema['properties'].get('legalForm') is not None:
|
||||
return_result.append(
|
||||
[{'Full Name': schema['properties']['name'][0],
|
||||
'Gender': gender,
|
||||
'Date Of Birth': str(schema['properties'].get('birthDate')),
|
||||
'Notes': f"{schema['links']['self']}\nLegal Form: {schema['properties']['legalForm'][0]}",
|
||||
'Entity Type': 'Person'},
|
||||
{uid: {'Resolution': 'Person Entity', 'Notes': ''}}])
|
||||
else:
|
||||
return_result.append(
|
||||
[{'Full Name': str(schema['properties']['name'][0]),
|
||||
'Gender': gender,
|
||||
'Date Of Birth': str(schema['properties']['birthDate'][0]),
|
||||
'Notes': schema['links']['self'],
|
||||
'Entity Type': 'Person'},
|
||||
{uid: {'Resolution': 'Person Entity', 'Notes': ''}}])
|
||||
if schema['properties'].get('registrationNumber') is not None:
|
||||
return_result.append(
|
||||
[{'Registration Number': str(schema['properties']['registrationNumber'][0]),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Company'},
|
||||
{index_of_child: {'Resolution': 'Aleph Registration Number', 'Notes': ''}}])
|
||||
if schema['properties'].get('country') is not None:
|
||||
return_result.append(
|
||||
[{'Country Name': str(
|
||||
pycountry.countries.get(alpha_2=schema['properties']['country'][0]).name),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Country'},
|
||||
{index_of_child: {'Resolution': 'Country of Origin', 'Notes': ''}}])
|
||||
if schema['properties'].get('addressEntity'):
|
||||
return_result.append(
|
||||
[{'Street Address': str(
|
||||
schema['properties']['addressEntity'][0]['properties']['full'][0]),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Address'},
|
||||
{index_of_child: {'Resolution': 'Address Entity', 'Notes': ''}}])
|
||||
else:
|
||||
return_result.append(
|
||||
[{'Street Address': str(schema['properties']['address'][0]),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Address'},
|
||||
{index_of_child: {'Resolution': 'Address Entity', 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'ID': str(schema['id']),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Aleph ID'},
|
||||
{index_of_child: {'Resolution': 'Aleph ID', 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'Phrase': str(schema['collection']['label']),
|
||||
'Notes': str(schema['collection']['summary']),
|
||||
'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'Aleph Collection', 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'ID': str(schema['collection']['collection_id']),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Aleph Collection ID'},
|
||||
{index_of_child: {'Resolution': 'Aleph Collection ID', 'Notes': ''}}])
|
||||
elif schema['schema'] == "Organization":
|
||||
return_result.append(
|
||||
[{'Organization Name': str(schema['properties']['name'][0]),
|
||||
'Registration Number': str(schema['properties']['registrationNumber'][0]),
|
||||
'Notes': f"{schema['links']['self']}\nLegal Form: {schema['properties']['legalForm'][0]}\n"
|
||||
f"Source URL: {schema['properties']['sourceUrl'][0]}",
|
||||
'Entity Type': 'Organization'},
|
||||
{uid: {'Resolution': 'Aleph Organisation Entity', 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'Country Name': str(
|
||||
pycountry.countries.get(alpha_2=schema['properties']['country'][0]).name),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Country'},
|
||||
{index_of_child: {'Resolution': "Aleph Organisation Country", 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'Street Address': str(schema['properties']['address'][0]),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Address'},
|
||||
{index_of_child: {'Resolution': "Aleph Organisation Address", 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'ID': str(schema['id']),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Aleph ID'},
|
||||
{index_of_child: {'Resolution': "Aleph Organisation ID", 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'Phrase': str(schema['collection']['label']),
|
||||
'Notes': str(schema['collection']['summary']),
|
||||
'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'Aleph Collection', 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'Phone Number': str(schema['properties']['phone'][0]),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Phone Number'},
|
||||
{index_of_child: {'Resolution': 'Phone Number', 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'Phrase': str(schema['properties']['classification'][0]),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'Organisation Classification', 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'Phrase': str(schema['collection']['collection_id']),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'Aleph Collection ID', 'Notes': ''}}])
|
||||
elif schema['schema'] == "Pages":
|
||||
if 'updated_at' in schema:
|
||||
date_created = schema['updated_at']
|
||||
else:
|
||||
date_created = schema['created_at']
|
||||
doc_name = 'Document: ' + schema['properties']['title'][0]
|
||||
entity_link = schema['links']['self']
|
||||
file_link = schema['links']['file']
|
||||
source_url = schema['properties']['sourceUrl'][0]
|
||||
return_result.append(
|
||||
[{'Phrase': doc_name,
|
||||
'Source': source_url,
|
||||
'Notes': 'Link to Aleph Entity: ' + entity_link + '\n\n' +
|
||||
'Link to document: ' + file_link,
|
||||
'Entity Type': 'Phrase',
|
||||
'Date Created': date_created},
|
||||
{uid: {'Resolution': 'Aleph Document', 'Notes': ''}}])
|
||||
elif schema['properties']['parent'][0]['schema'] == "Person":
|
||||
gender = str(schema['properties']['parent'][0]['properties'].get('gender')[0])
|
||||
if schema['properties']['parent'][0]['properties'].get('legalForm') is not None:
|
||||
return_result.append(
|
||||
[{'Full Name': schema['properties']['parent'][0]['properties']['name'][0],
|
||||
'Gender': gender,
|
||||
'Date Of Birth': str(schema['properties']['parent'][0]['properties']['birthDate'][0]),
|
||||
'Notes': f"{schema['properties']['parent'][0]['links']['self']}\nLegal Form: "
|
||||
f"{schema['properties']['parent'][0]['properties']['legalForm'][0]}",
|
||||
'Entity Type': 'Person'},
|
||||
{uid: {'Resolution': 'Aleph Person Entity', 'Notes': ''}}])
|
||||
else:
|
||||
return_result.append(
|
||||
[{'Full Name': schema['properties']['parent'][0]['properties']['name'][0],
|
||||
'Gender': gender,
|
||||
'Date Of Birth': str(schema['properties']['parent'][0]['properties']['birthDate'][0]),
|
||||
'Notes': schema['properties']['parent'][0]['links']['self'],
|
||||
'Entity Type': 'Person'},
|
||||
{uid: {'Resolution': 'Aleph Person Entity', 'Notes': ''}}])
|
||||
if schema['properties']['parent'][0]['properties'].get('registrationNumber') is not None:
|
||||
return_result.append(
|
||||
[{'Registration Number': str(
|
||||
schema['properties']['parent'][0]['properties']['registrationNumber'][0]),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Company'},
|
||||
{index_of_child: {'Resolution': 'Company Registration Number', 'Notes': ''}}])
|
||||
if schema['properties']['parent'][0]['properties'].get('country') is not None:
|
||||
return_result.append(
|
||||
[{'Country Name': str(
|
||||
pycountry.countries.get(
|
||||
alpha_2=schema['properties']['parent'][0]['properties']['country'][0]).name),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Country'},
|
||||
{index_of_child: {'Resolution': 'Country', 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'ID': str(schema['properties']['parent'][0]['id']),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Aleph ID'},
|
||||
{index_of_child: {'Resolution': 'Aleph ID', 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'Phrase': str(schema['properties']['parent'][0]['collection']['label']),
|
||||
'Notes': str(schema['properties']['parent'][0]['collection']['summary']),
|
||||
'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'Aleph Collection Entity', 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'Phrase': str(schema['properties']['parent'][0]['collection']['collection_id']),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'Aleph Collection ID', 'Notes': ''}}])
|
||||
index_of_child_of_child = len(return_result)
|
||||
return_result.append(
|
||||
[{'Company Name': str(schema['properties']['name'][0]),
|
||||
'Notes': schema['links']['self'],
|
||||
'Entity Type': 'Company'},
|
||||
{index_of_child: {'Resolution': 'Aleph Company Entity', 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'Street Address': str(schema['properties']['addressEntity'][0]['properties']['full'][0]),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Address'},
|
||||
{index_of_child_of_child: {'Resolution': 'Aleph Person Address', 'Notes': ''}}])
|
||||
for country_code in schema['collection']['countries']:
|
||||
return_result.append(
|
||||
[{'Country Name': str(pycountry.countries.get(alpha_2=country_code).name),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Country'},
|
||||
{index_of_child_of_child: {'Resolution': 'Country', 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'Phrase': str(schema['collection']['label']),
|
||||
'Notes': str(schema['collection']['summary']),
|
||||
'Entity Type': 'Phrase'},
|
||||
{index_of_child_of_child: {'Resolution': 'Aleph Collection Entity', 'Notes': ''}}])
|
||||
return_result.append(
|
||||
[{'ID': str(schema['collection']['collection_id']),
|
||||
'Notes': '',
|
||||
'Entity Type': 'Aleph Collection ID'},
|
||||
{index_of_child_of_child: {'Resolution': 'Aleph Entity Search', 'Notes': ''}}])
|
||||
except (TypeError, KeyError):
|
||||
# print(repr(e))
|
||||
continue
|
||||
return return_result
|
||||
@@ -1,98 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class GetCollectionByID:
|
||||
name = "Get Collections By ID"
|
||||
category = "Aleph OCCRP"
|
||||
description = "Find information about Collections and their IDs"
|
||||
originTypes = {'Phrase'}
|
||||
resultTypes = {'Phrase. Person, Address, Phone Number, Email Address, Country, Bank Account'}
|
||||
parameters = {'Aleph Disclaimer': {'description': 'The content on Aleph is provided for general information only.\n'
|
||||
'It is not intended to amount to advice on which you should place'
|
||||
'sole and entire reliance.\n'
|
||||
'We recommend that you conduct your own independent fact checking'
|
||||
'against the data and materials that you access on Aleph.\n'
|
||||
'Aleph API is not a replacement for traditional due diligence '
|
||||
'checks and know-your-customer background checks.',
|
||||
'type': 'String',
|
||||
'value': 'Type "Accept" (without quotes) to confirm your understanding.',
|
||||
'global': True}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import time
|
||||
import requests
|
||||
from requests_futures.sessions import FuturesSession
|
||||
from concurrent.futures import as_completed
|
||||
|
||||
returnResults = []
|
||||
futures = []
|
||||
uidList = []
|
||||
|
||||
if parameters['Aleph Disclaimer'] != 'Accept':
|
||||
return "Please Accept the Terms for Aleph."
|
||||
|
||||
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
|
||||
with FuturesSession(max_workers=15) as session:
|
||||
for entity in entityJsonList:
|
||||
uidList.append(entity['uid'])
|
||||
primary_field = entity[list(entity)[1]].strip()
|
||||
url = f"https://aleph.occrp.org/api/2/collections/{primary_field}"
|
||||
time.sleep(1)
|
||||
futures.append(session.get(url, headers=headers))
|
||||
for future in as_completed(futures):
|
||||
uid = uidList[futures.index(future)]
|
||||
try:
|
||||
response = future.result().json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
if response['statistics']['names'].get('values') is not None:
|
||||
nameKeys = list(response['statistics']['names'].get('values').keys())
|
||||
for nameKey in nameKeys:
|
||||
returnResults.append([{'Full Name': str(nameKey),
|
||||
'Entity Type': 'Person'},
|
||||
{uid: {'Resolution': 'Person Entity',
|
||||
'Notes': ''}}])
|
||||
if response['statistics']['addresses'].get('values') is not None:
|
||||
addressKeys = list(response['statistics']['addresses'].get('values').keys())
|
||||
for addressKey in addressKeys:
|
||||
returnResults.append([{'Street Address': str(addressKey),
|
||||
'Entity Type': 'Address'},
|
||||
{uid: {'Resolution': 'Address Entity',
|
||||
'Notes': ''}}])
|
||||
if response['statistics']['phones'].get('values') is not None:
|
||||
phoneKeys = list(response['statistics']['phones'].get('values').keys())
|
||||
for phoneKey in phoneKeys:
|
||||
returnResults.append([{'Phone Number': str(phoneKey),
|
||||
'Entity Type': 'Phone Number'},
|
||||
{uid: {'Resolution': 'Phone Number Entity',
|
||||
'Notes': ''}}])
|
||||
|
||||
if response['statistics']['emails'].get('values') is not None:
|
||||
emailKeys = list(response['statistics']['emails'].get('values').keys())
|
||||
for emailKey in emailKeys:
|
||||
returnResults.append([{'Email Address': str(emailKey),
|
||||
'Entity Type': 'Email Address'},
|
||||
{uid: {'Resolution': 'Email Address Entity',
|
||||
'Notes': ''}}])
|
||||
if response['statistics']['countries'].get('values') is not None:
|
||||
countriesKeys = list(response['statistics']['countries'].get('values').keys())
|
||||
for countriesKey in countriesKeys:
|
||||
returnResults.append([{'Country Name': str(countriesKey),
|
||||
'Entity Type': 'Country'},
|
||||
{uid: {'Resolution': 'Country Entity',
|
||||
'Notes': ''}}])
|
||||
if response['statistics']['languages'].get('values') is not None:
|
||||
languagesKeys = list(response['statistics']['languages'].get('values').keys())
|
||||
for languagesKey in languagesKeys:
|
||||
returnResults.append([{'Phrase': str(languagesKey),
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': 'Language Entity',
|
||||
'Notes': ''}}])
|
||||
if response['statistics']['ibans'].get('values') is not None:
|
||||
ibansKeys = list(response['statistics']['ibans'].get('values').keys())
|
||||
for ibansKey in ibansKeys:
|
||||
returnResults.append([{'Account Number': str(ibansKey),
|
||||
'Entity Type': 'Bank Account'},
|
||||
{uid: {'Resolution': 'IBAN Entity',
|
||||
'Notes': ''}}])
|
||||
return returnResults
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class GetCollectionsInfo:
|
||||
name = "Get Collections Info"
|
||||
category = "Aleph OCCRP"
|
||||
description = "Find information about Collections and their IDs"
|
||||
originTypes = {'Phrase'}
|
||||
resultTypes = {'Phrase, Aleph ID'}
|
||||
parameters = {'Max Results': {'description': 'Please enter the maximum number of results to return.',
|
||||
'type': 'String',
|
||||
'default': '1'},
|
||||
'Aleph Disclaimer': {'description': 'The content on Aleph is provided for general information only.\n'
|
||||
'It is not intended to amount to advice on which you should place'
|
||||
'sole and entire reliance.\n'
|
||||
'We recommend that you conduct your own independent fact checking'
|
||||
'against the data and materials that you access on Aleph.\n'
|
||||
'Aleph API is not a replacement for traditional due diligence '
|
||||
'checks and know-your-customer background checks.',
|
||||
'type': 'String',
|
||||
'value': 'Type "Accept" (without quotes) to confirm your understanding.',
|
||||
'global': True}
|
||||
}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import time
|
||||
import requests
|
||||
from requests_futures.sessions import FuturesSession
|
||||
from concurrent.futures import as_completed
|
||||
|
||||
returnResults = []
|
||||
futures = []
|
||||
uidList = []
|
||||
|
||||
if parameters['Aleph Disclaimer'] != 'Accept':
|
||||
return "Please Accept the Terms for Aleph."
|
||||
|
||||
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
|
||||
|
||||
try:
|
||||
maxResults = int(parameters['Max Results'])
|
||||
except ValueError:
|
||||
return "The value for parameter 'Max Results' is not a valid integer."
|
||||
if maxResults <= 0:
|
||||
return []
|
||||
with FuturesSession(max_workers=15) as session:
|
||||
for entity in entityJsonList:
|
||||
uidList.append(entity['uid'])
|
||||
url = f"https://aleph.occrp.org/api/2/collections?offset=0&limit=300&page"
|
||||
time.sleep(1)
|
||||
futures.append(session.get(url, headers=headers))
|
||||
for future in as_completed(futures):
|
||||
uid = uidList[futures.index(future)]
|
||||
try:
|
||||
response = future.result().json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
|
||||
collections = response['results'][:maxResults]
|
||||
|
||||
for collection in collections:
|
||||
index_of_child = len(returnResults)
|
||||
returnResults.append([{'Phrase': collection['label'],
|
||||
'Notes': str(collection.get('summary')),
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': 'Aleph Collection Name',
|
||||
'Notes': ''}}])
|
||||
|
||||
returnResults.append([{'ID': collection['id'],
|
||||
'Entity Type': 'Aleph ID'},
|
||||
{index_of_child: {'Resolution': 'Aleph Collection ID',
|
||||
'Notes': ''}}])
|
||||
return returnResults
|
||||
@@ -1,102 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class GetSimilarEntities:
|
||||
name = "Get Similar Entities"
|
||||
category = "Aleph OCCRP"
|
||||
description = "Find information about similar entities"
|
||||
originTypes = {'Phrase', 'Person', 'Politically Exposed Person'}
|
||||
resultTypes = {'Phrase', 'Person', 'Address', 'Aleph ID'}
|
||||
parameters = {'Aleph Disclaimer': {'description': 'The content on Aleph is provided for general information only.\n'
|
||||
'It is not intended to amount to advice on which you should place'
|
||||
'sole and entire reliance.\n'
|
||||
'We recommend that you conduct your own independent fact checking'
|
||||
'against the data and materials that you access on Aleph.\n'
|
||||
'Aleph API is not a replacement for traditional due diligence '
|
||||
'checks and know-your-customer background checks.',
|
||||
'type': 'String',
|
||||
'value': 'Type "Accept" (without quotes) to confirm your understanding.',
|
||||
'global': True}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import time
|
||||
import requests
|
||||
import pycountry
|
||||
from requests_futures.sessions import FuturesSession
|
||||
from concurrent.futures import as_completed
|
||||
|
||||
returnResults = []
|
||||
futures = []
|
||||
uidList = []
|
||||
|
||||
if parameters['Aleph Disclaimer'] != 'Accept':
|
||||
return "Please Accept the Terms for Aleph."
|
||||
|
||||
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
|
||||
with FuturesSession(max_workers=15) as session:
|
||||
for entity in entityJsonList:
|
||||
uidList.append(entity['uid'])
|
||||
primary_field = entity[list(entity)[1]].strip()
|
||||
url = f"https://aleph.occrp.org/api/2/entities/{primary_field}/similar"
|
||||
time.sleep(1)
|
||||
futures.append(session.get(url, headers=headers))
|
||||
for future in as_completed(futures):
|
||||
uid = uidList[futures.index(future)]
|
||||
try:
|
||||
response = future.result().json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
entities = response['results']
|
||||
for schema in entities:
|
||||
if schema['entity']['schema'] == 'Person':
|
||||
index_of_child = len(returnResults)
|
||||
returnResults.append([{'Full Name': ' '.join(map(str, schema['entity']['properties']['name'])),
|
||||
'Gender': ' '.join(map(str, schema['entity']['properties']['gender'])),
|
||||
'Notes': ' '.join(map(str, schema['entity']['properties']['legalForm'])),
|
||||
'Entity Type': 'Person'},
|
||||
{uid: {'Resolution': 'Person Entity',
|
||||
'Notes': ''}}])
|
||||
country = pycountry.countries.get(alpha_2=schema['entity']['properties']['country'][0]).name
|
||||
returnResults.append([{'Street Address': schema['entity']['properties']['addressEntity'][0]
|
||||
['properties']['full'][0],
|
||||
'Postal Code': schema['entity']['properties']['addressEntity'][0]
|
||||
['properties']['postalCode'][0],
|
||||
'Country': country,
|
||||
'Entity Type': 'Address'},
|
||||
{index_of_child: {'Resolution': 'Address',
|
||||
'Notes': ''}}])
|
||||
returnResults.append([{'Phrase': schema['entity']['collection']['label'],
|
||||
'Notes': schema['entity']['collection']['summary'],
|
||||
'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'Location in Database',
|
||||
'Notes': ''}}])
|
||||
|
||||
returnResults.append([{'ID': schema['entity']['id'],
|
||||
'Entity Type': 'Aleph ID'},
|
||||
{index_of_child: {'Resolution': 'ID in Database',
|
||||
'Notes': ''}}])
|
||||
|
||||
elif schema['entity']['schema'] == 'Company':
|
||||
index_of_child = len(returnResults)
|
||||
returnResults.append([{'Company Name': ' '.join(map(str, schema['entity']['properties']['name'])),
|
||||
'Notes': str(schema['entity']['properties']['status']),
|
||||
'Entity Type': 'Company'},
|
||||
{uid: {'Resolution': 'Company Entity',
|
||||
'Notes': ''}}])
|
||||
country = pycountry.countries.get(alpha_2=schema['entity']['properties']['country'][0]).name
|
||||
returnResults.append([{'Street Address': str(schema['entity']['properties'].get('address')),
|
||||
'Country': country,
|
||||
'Entity Type': 'Address'},
|
||||
{index_of_child: {'Resolution': 'Address',
|
||||
'Notes': ''}}])
|
||||
returnResults.append([{'Phrase': schema['entity']['collection']['label'],
|
||||
'Notes': schema['entity']['collection']['summary'],
|
||||
'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'Location in Database',
|
||||
'Notes': ''}}])
|
||||
|
||||
returnResults.append([{'ID': schema['entity']['id'],
|
||||
'Entity Type': 'Aleph ID'},
|
||||
{index_of_child: {'Resolution': 'ID in Database',
|
||||
'Notes': ''}}])
|
||||
return returnResults
|
||||
@@ -1,3 +0,0 @@
|
||||
requests
|
||||
pycountry
|
||||
requests-futures
|
||||
@@ -1,280 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# User needs to be in docker group or to have root privileges
|
||||
|
||||
class Amass_Domain:
|
||||
name = "Amass Domain Scan"
|
||||
category = "Network Infrastructure"
|
||||
description = "Find information about a particular domain. Requires Docker to be installed."
|
||||
originTypes = {'Domain'}
|
||||
resultTypes = {'IP Address', 'Phrase', 'Autonomous System', 'Domain', 'IPv6 Address'}
|
||||
parameters = {'VirusTotal API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://virustotal.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'AlienVault API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://otx.alienvault.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'BinaryEdge API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://app.binaryedge.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'C99 API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://c99.nl.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Censys API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://censys.io.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Chaos API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://chaos.projectdiscovery.io.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Cloudflare API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://cloudflare.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'DNSDB API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://dnsdb.info.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'GitHub API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://github.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Hunter API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://hunter.io.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'IPInfo Access Token': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://ipinfo.io.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'NetworksDB API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://networksdb.io.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'PassiveTotal API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://passivetotal.com .',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'ReconDev API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://recon.dev.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'SecurityTrails API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://securitytrails.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Shodan API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://shodan.io.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Spyse API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://spyse.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'ThreatBook API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://threatbook.cn.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Umbrella API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://umbrella.cisco.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'URLScan API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://urlscan.io.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'WhoisXMLAPI API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://whoisxmlapi.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'ZETAlytics API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://zetalytics.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'ZoomEye API Key': {'description': 'Please Enter the Username and password with a space '
|
||||
'in between',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'FacebookCT API Key': {'description': 'Please Enter the api key and secret with a space '
|
||||
'in between. Obtain them at https://developer.facebook.com',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Twitter API Key': {'description': 'Please Enter the api key and secret with a space '
|
||||
'in between. Obtain them at https://developer.twitter.com',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'ReconDev.free API Key': {
|
||||
'description':
|
||||
'Please Enter the api key under your profile after signing up on https://recon.dev',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'ReconDev.paid API Key': {
|
||||
'description':
|
||||
'Please Enter the api key under your profile after signing up on https://recon.dev',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
from pathlib import Path
|
||||
import json
|
||||
from ipaddress import ip_address, IPv4Address, IPv6Address
|
||||
import docker
|
||||
import tempfile
|
||||
from docker.errors import APIError
|
||||
|
||||
return_result = []
|
||||
# Generate Config as a temporary file:
|
||||
with tempfile.TemporaryDirectory() as tempDir:
|
||||
tempPath = Path(tempDir).absolute()
|
||||
config = tempfile.NamedTemporaryFile(mode='w+t', prefix='Amass',
|
||||
suffix='Config',
|
||||
dir=tempPath)
|
||||
config.write("share = true\n")
|
||||
config.write("[scope]\n")
|
||||
config.write("port = 80\n")
|
||||
config.write("port = 443\n")
|
||||
config.write("[data_sources]\n")
|
||||
config.write("minimum_ttl = 1440\n")
|
||||
for parameter in self.parameters:
|
||||
if parameters[f'{parameter}'] != 'None':
|
||||
field1 = f"[data_sources.{parameter}]"
|
||||
field2 = f"[data_sources.{parameter}.Credentials]"
|
||||
if parameter == "ZoomEye":
|
||||
username, password = parameters[parameter].split(' ', 1)
|
||||
config.write(f"{field1}\n")
|
||||
config.write(f"{field2}\n")
|
||||
config.write(f"username = {username}\n")
|
||||
config.write(f"password = {password}\n")
|
||||
elif parameter == "FacebookCT":
|
||||
field3, secret = parameters[parameter].split(' ', 1)
|
||||
config.write(f"{field1}\n")
|
||||
config.write(f"[data_sources.{parameter}.app1\n")
|
||||
config.write(f"apikey = \"{field3}\"\n")
|
||||
config.write(f"secret = {secret}\n")
|
||||
elif parameter == "Twitter":
|
||||
field3, secret = parameters[parameter].split(' ', 1)
|
||||
config.write(f"{field1}\n")
|
||||
config.write(f"[data_sources.{parameter}.account1\n")
|
||||
config.write(f"apikey = \"{field3}\"\n")
|
||||
config.write(f"secret = {secret}\n")
|
||||
elif parameter == "ReconDev.paid":
|
||||
field3 = parameters[f'{parameter}']
|
||||
config.write(f"{field1}\n")
|
||||
config.write(f"[data_sources.{parameter}.paid\n")
|
||||
config.write(f"apikey = \"{field3}\"\n")
|
||||
elif parameter == "ReconDev.free":
|
||||
field3 = parameters[f'{parameter}']
|
||||
config.write(f"{field1}\n")
|
||||
config.write(f"[data_sources.{parameter}.free\n")
|
||||
config.write(f"apikey = \"{field3}\"\n")
|
||||
else:
|
||||
field3 = parameters[f'{parameter}']
|
||||
config.write(f"{field1}\n")
|
||||
config.write(f"{field2}\n")
|
||||
config.write(f"apikey = \"{field3}\"\n")
|
||||
path_to_config = Path(config.name).name
|
||||
config.seek(0)
|
||||
for entity in entityJsonList:
|
||||
primary_field = entity["Domain Name"].strip()
|
||||
try:
|
||||
client = docker.from_env()
|
||||
container = client.containers.run("caffix/amass:latest",
|
||||
f"enum -src -d {primary_field} "
|
||||
f"-config /.config/amass/{path_to_config}",
|
||||
volumes={
|
||||
str(tempPath): {'bind': '/.config/amass',
|
||||
'mode': 'rw'}},
|
||||
remove=True)
|
||||
jsonFile = tempPath / 'amass.json'
|
||||
jsonContents = ""
|
||||
if jsonFile.exists():
|
||||
with open(jsonFile, 'r') as jsonFileHandler:
|
||||
jsonContents = jsonFileHandler.read()
|
||||
client.close()
|
||||
except (APIError, docker.errors.ContainerError) as error:
|
||||
return "Something happened to the docker container - Cannot continue: " + str(error)
|
||||
uid = entity['uid']
|
||||
for dictionary in jsonContents.splitlines():
|
||||
index_of_child = len(return_result)
|
||||
line_dictionary = json.loads(dictionary)
|
||||
size = len(line_dictionary['addresses'])
|
||||
return_result.append([{'Domain Name': str(line_dictionary['name']),
|
||||
'Entity Type': 'Domain'},
|
||||
{uid: {'Resolution': 'Amass Domain Scan', 'Notes': ''}}])
|
||||
for ip in range(size):
|
||||
if type(ip_address(line_dictionary['addresses'][ip]['ip'])) is IPv4Address:
|
||||
return_result.append([{'IP Address': str(line_dictionary['addresses'][ip]['ip']),
|
||||
'Entity Type': 'IP Address'},
|
||||
{index_of_child: {'Resolution': 'Amass IP Address', 'Notes': ''}}])
|
||||
elif type(ip_address(line_dictionary['addresses'][ip]['ip'])) is IPv6Address:
|
||||
return_result.append([{'IPv6 Address': str(line_dictionary['addresses'][ip]['ip']),
|
||||
'Entity Type': 'IPv6 Address'},
|
||||
{index_of_child: {'Resolution': 'Amass IPv6 Address', 'Notes': ''}}])
|
||||
return_result.append([{'AS Number': "AS" + str(line_dictionary['addresses'][ip]['asn']),
|
||||
'ASN Cidr': str(line_dictionary['addresses'][ip]['cidr']),
|
||||
'Entity Type': 'Autonomous System'},
|
||||
{index_of_child: {'Resolution': 'Amass Autonomous System', 'Notes': ''}}])
|
||||
return_result.append([{'Phrase': str(line_dictionary['addresses'][ip]['desc']),
|
||||
'Entity Type': 'Phrase'},
|
||||
{index_of_child: {'Resolution': 'Amass Domain Scan Description',
|
||||
'Notes': ''}}])
|
||||
config.close()
|
||||
return return_result
|
||||
@@ -1,285 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# User needs to be in docker group or have root privileges
|
||||
|
||||
class Amass_Intel:
|
||||
name = "Amass Intel Scan"
|
||||
category = "Network Infrastructure"
|
||||
description = "Find information about a particular domain. Requires Docker to be installed."
|
||||
originTypes = {'Domain', 'IP Address', 'Autonomous System'}
|
||||
resultTypes = {'Domain'}
|
||||
parameters = {'VirusTotal API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://virustotal.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'AlienVault API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://otx.alienvault.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'BinaryEdge API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://app.binaryedge.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'C99 API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://c99.nl.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Censys API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://censys.io.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Chaos API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://chaos.projectdiscovery.io.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Cloudflare API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://cloudflare.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'DNSDB API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://dnsdb.info.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'GitHub API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://github.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Hunter API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://hunter.io.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'IPInfo Access Token': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://ipinfo.io.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'NetworksDB API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://networksdb.io.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'PassiveTotal API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://passivetotal.com .',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'ReconDev API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://recon.dev.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'SecurityTrails API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://securitytrails.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Shodan API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://shodan.io.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Spyse API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://spyse.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'ThreatBook API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://threatbook.cn.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Umbrella API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://umbrella.cisco.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'URLScan API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://urlscan.io.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'WhoisXMLAPI API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://whoisxmlapi.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'ZETAlytics API Key': {'description': 'Enter your api key under your profile after'
|
||||
' signing up on https://zetalytics.com.',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'ZoomEye API Key': {'description': 'Please Enter the Username and password with a space '
|
||||
'in between',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'FacebookCT API Key': {'description': 'Please Enter the api key and secret with a space '
|
||||
'in between. Obtain them at https://developer.facebook.com',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'Twitter API Key': {'description': 'Please Enter the api key and secret with a space '
|
||||
'in between. Obtain them at https://developer.twitter.com',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'ReconDev.free API Key': {
|
||||
'description':
|
||||
'Please Enter the api key under your profile after signing up on https://recon.dev',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'},
|
||||
'ReconDev.paid API Key': {
|
||||
'description':
|
||||
'Please Enter the api key under your profile after signing up on https://recon.dev',
|
||||
'type': 'String',
|
||||
'value': 'None',
|
||||
'global': True,
|
||||
'default': 'None'}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
from pathlib import Path
|
||||
import json
|
||||
from ipaddress import ip_address, IPv4Address, IPv6Address
|
||||
import docker
|
||||
import tempfile
|
||||
from docker.errors import APIError
|
||||
|
||||
return_result = []
|
||||
# Generate Config as a temporary file:
|
||||
with tempfile.TemporaryDirectory() as tempDir:
|
||||
tempPath = Path(tempDir).absolute()
|
||||
config = tempfile.NamedTemporaryFile(mode='w+t', prefix='Amass',
|
||||
suffix='Config',
|
||||
dir=tempPath)
|
||||
config.write("share = true\n")
|
||||
config.write("[scope]\n")
|
||||
config.write("port = 80\n")
|
||||
config.write("port = 443\n")
|
||||
config.write("[data_sources]\n")
|
||||
config.write("minimum_ttl = 1440\n")
|
||||
for parameter in parameters:
|
||||
if parameters[f'{parameter}'] != 'None':
|
||||
field1 = f"[data_sources.{parameter}]"
|
||||
field2 = f"[data_sources.{parameter}.Credentials]"
|
||||
if parameter == "ZoomEye":
|
||||
username, password = parameters[parameter].split(' ', 1)
|
||||
config.write(f"{field1}\n")
|
||||
config.write(f"{field2}\n")
|
||||
config.write(f"username = {username}\n")
|
||||
config.write(f"password = {password}\n")
|
||||
elif parameter == "FacebookCT":
|
||||
field3, secret = parameters[parameter].split(' ', 1)
|
||||
config.write(f"{field1}\n")
|
||||
config.write(f"[data_sources.{parameter}.app1\n")
|
||||
config.write(f"apikey = \"{field3}\"\n")
|
||||
config.write(f"secret = {secret}\n")
|
||||
elif parameter == "Twitter":
|
||||
field3, secret = parameters[parameter].split(' ', 1)
|
||||
config.write(f"{field1}\n")
|
||||
config.write(f"[data_sources.{parameter}.account1\n")
|
||||
config.write(f"apikey = \"{field3}\"\n")
|
||||
config.write(f"secret = {secret}\n")
|
||||
elif parameter == "ReconDev.paid":
|
||||
field3 = parameters[f'{parameter}']
|
||||
config.write(f"{field1}\n")
|
||||
config.write(f"[data_sources.{parameter}.paid\n")
|
||||
config.write(f"apikey = \"{field3}\"\n")
|
||||
elif parameter == "ReconDev.free":
|
||||
field3 = parameters[f'{parameter}']
|
||||
config.write(f"{field1}\n")
|
||||
config.write(f"[data_sources.{parameter}.free\n")
|
||||
config.write(f"apikey = \"{field3}\"\n")
|
||||
else:
|
||||
field3 = parameters[f'{parameter}']
|
||||
config.write(f"{field1}\n")
|
||||
config.write(f"{field2}\n")
|
||||
config.write(f"apikey = \"{field3}\"\n")
|
||||
path_to_config = "/" + Path(config.name).name
|
||||
config.seek(0)
|
||||
for entity in entityJsonList:
|
||||
primary_field = entity[list(entity)[1]].strip()
|
||||
try:
|
||||
client = docker.from_env()
|
||||
if entity['Entity Type'] == "Domain":
|
||||
container = client.containers.run("caffix/amass:latest",
|
||||
f"intel -whois -d {primary_field} -config /.config/amass"
|
||||
f"{path_to_config}",
|
||||
volumes={
|
||||
str(tempPath): {'bind': '/.config/amass',
|
||||
'mode': 'rw'}},
|
||||
remove=True)
|
||||
elif entity['Entity Type'] == "IP Address":
|
||||
try:
|
||||
ip_address(primary_field)
|
||||
except ValueError:
|
||||
return "The Entity Provided isn't a valid IP Address"
|
||||
container = client.containers.run("caffix/amass:latest",
|
||||
f"intel -addr {primary_field} -config "
|
||||
f"/.config/amass{path_to_config}",
|
||||
volumes={
|
||||
str(tempPath): {'bind': '/.config/amass',
|
||||
'mode': 'rw'}},
|
||||
remove=True)
|
||||
elif entity['Entity Type'] == "Autonomous System":
|
||||
if primary_field.startswith('AS'):
|
||||
primary_field = primary_field[2:]
|
||||
container = client.containers.run("caffix/amass:latest",
|
||||
f"intel -asn {primary_field}"
|
||||
f" -config /.config/amass{path_to_config}",
|
||||
volumes={
|
||||
str(tempPath): {'bind': '/.config/amass',
|
||||
'mode': 'rw'}},
|
||||
remove=True)
|
||||
textFile = tempPath / 'amass.txt'
|
||||
textContents = ""
|
||||
if textFile.exists():
|
||||
with open(textFile, 'r') as textFileHandler:
|
||||
textContents = textFileHandler.read()
|
||||
|
||||
client.close()
|
||||
except (APIError, docker.errors.ContainerError) as error:
|
||||
return "Something happened to the docker container - Cannot continue: " + str(error)
|
||||
uid = entity['uid']
|
||||
for newDomain in textContents.splitlines():
|
||||
return_result.append([{'Domain Name': newDomain.strip(),
|
||||
'Entity Type': 'Domain'},
|
||||
{uid: {'Resolution': 'Amass Intel Scan', 'Notes': ''}}])
|
||||
|
||||
config.close()
|
||||
return return_result
|
||||
@@ -1 +0,0 @@
|
||||
docker
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class BigMatch:
|
||||
name = "BigMatch Search"
|
||||
category = "Secrets & Leaks"
|
||||
description = "Find information about a file using https://bigmatch.rev.ng/static/index.html"
|
||||
originTypes = {"Image", "Document", "Archive"}
|
||||
resultTypes = {'Website'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
from pathlib import Path
|
||||
from playwright.sync_api import sync_playwright, TimeoutError, Error
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
return_result = []
|
||||
|
||||
url = "https://bigmatch.rev.ng/static/index.html"
|
||||
failString = 'Too many strings in binary?'
|
||||
successString = 'Results:'
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.firefox.launch()
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0'
|
||||
)
|
||||
page = context.new_page()
|
||||
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
file_path = Path(parameters['Project Files Directory']) / entity["File Path"]
|
||||
file_path = file_path.absolute()
|
||||
if not (file_path.exists() and file_path.is_file()):
|
||||
continue
|
||||
page.wait_for_timeout(3000)
|
||||
|
||||
for _ in range(3):
|
||||
try:
|
||||
page.goto(url, wait_until="networkidle", timeout=10000)
|
||||
inputLocator = page.locator("input")
|
||||
inputLocator.set_input_files([str(file_path)])
|
||||
page.wait_for_timeout(3000)
|
||||
soup = BeautifulSoup(page.content(), 'lxml')
|
||||
soupText = soup.get_text()
|
||||
while (failString not in soupText) and (successString not in soupText):
|
||||
page.wait_for_timeout(1000)
|
||||
soup = BeautifulSoup(page.content(), 'lxml')
|
||||
soupText = soup.get_text()
|
||||
if failString in soupText:
|
||||
return []
|
||||
for link in soup.find_all('a'):
|
||||
potentialLink = link.get('href', None)
|
||||
if potentialLink is not None:
|
||||
if 'github' in potentialLink:
|
||||
return_result.append([{'URL': potentialLink, 'Entity Type': 'Website'},
|
||||
{uid: {'Resolution': 'BigMatch Github Link', 'Notes': ''}}])
|
||||
|
||||
break
|
||||
except TimeoutError:
|
||||
pass
|
||||
except Error:
|
||||
break
|
||||
page.close()
|
||||
browser.close()
|
||||
return return_result
|
||||
@@ -1,2 +0,0 @@
|
||||
bs4
|
||||
playwright
|
||||
@@ -1,64 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class BinaryEdgeHost:
|
||||
name = "BinaryEdge Host Query"
|
||||
category = "Network Infrastructure"
|
||||
description = "Get information about a host from BinaryEdge."
|
||||
originTypes = {"IP Address", "IPv6 Address"}
|
||||
resultTypes = {'Port'}
|
||||
parameters = {'BinaryEdge API Key': {'description': "Enter your BinaryEdge API key. Sign up for one at "
|
||||
"https://www.binaryedge.io/",
|
||||
'type': 'String',
|
||||
'value': '',
|
||||
'global': True}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import json
|
||||
|
||||
baseURL = 'https://api.binaryedge.io/v2/query/ip/'
|
||||
requestHeaders = {'X-Key': parameters['BinaryEdge API Key'].strip()}
|
||||
|
||||
returnResults = []
|
||||
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
if entity['Entity Type'] == 'IP Address':
|
||||
primaryField = entity['IP Address']
|
||||
elif entity['Entity Type'] == 'IPv6 Address':
|
||||
primaryField = entity['IPv6 Address']
|
||||
else:
|
||||
continue
|
||||
infoRequest = requests.get(baseURL + primaryField, headers=requestHeaders)
|
||||
statusCode = infoRequest.status_code
|
||||
|
||||
if statusCode == 401:
|
||||
return "The BinaryEdge API key provided is not valid."
|
||||
elif statusCode == 403:
|
||||
return "The BinaryEdge API key provided does not have permission to access this resource."
|
||||
elif statusCode != 200:
|
||||
continue
|
||||
requestContent = json.loads(infoRequest.content)
|
||||
|
||||
for event in requestContent['events']:
|
||||
for result in event['results']:
|
||||
originDetails = result['origin']
|
||||
targetDetails = result['target']
|
||||
resultDetails = result['result']
|
||||
if 'state' not in resultDetails['data']:
|
||||
# Discard return result if it doesn't actually give us useful info about the state of the port.
|
||||
# This happens in cases where the API returns stuff like the ciphers used in an SSH service.
|
||||
# There seems to always be a result with the simple port info, so we will use that one.
|
||||
continue
|
||||
|
||||
returnResults.append([{'Port': targetDetails['ip'] + ':' + str(targetDetails['port']) + ':' +
|
||||
targetDetails['protocol'],
|
||||
'State': resultDetails['data']['state']['state'],
|
||||
'Banner': resultDetails['data']['service'].get('banner', 'N/A'),
|
||||
'Product': resultDetails['data']['service'].get('product', 'Unknown'),
|
||||
'Entity Type': 'Port'},
|
||||
{uid: {'Resolution': 'BinaryEdge Scan Timestamp: ' + str(originDetails['ts']),
|
||||
'Notes': ''}}])
|
||||
|
||||
return returnResults
|
||||
@@ -1 +0,0 @@
|
||||
requests
|
||||
@@ -1,34 +0,0 @@
|
||||
<HIBP>
|
||||
<Data_Breach>
|
||||
<Attributes>
|
||||
<Attribute default="Breach Name" check="String" primary="True">Breach Name</Attribute>
|
||||
<Attribute default="Breach Title" check="String" primary="False">Breach Title</Attribute>
|
||||
<Attribute default="Breach Domain" check="String" primary="False">Breach Domain</Attribute>
|
||||
<Attribute default="0" check="Numbers" primary="False">Breach Pwn Count</Attribute>
|
||||
<Attribute default="Breach Description" check="String" primary="False">Breach Description</Attribute>
|
||||
<Attribute default="False" check="String" primary="False">Breach Is Sensitive</Attribute>
|
||||
<Attribute default="False" check="String" primary="False">Breach Is Verified</Attribute>
|
||||
<Attribute default="False" check="String" primary="False">Breach Is Fabricated</Attribute>
|
||||
<Attribute default="False" check="String" primary="False">Breach Is Retired</Attribute>
|
||||
<Attribute default="False" check="String" primary="False">Breach Is Spam List</Attribute>
|
||||
<Attribute default="False" check="String" primary="False">Breach Is Malware</Attribute>
|
||||
<Attribute default="Unknown" check="String" primary="False">Breach Added Date</Attribute>
|
||||
<Attribute default="Unknown" check="String" primary="False">Breach Modified Date</Attribute>
|
||||
</Attributes>
|
||||
<Icon>
|
||||
Default.svg
|
||||
</Icon>
|
||||
</Data_Breach>
|
||||
<Paste_Data_Leak>
|
||||
<Attributes>
|
||||
<Attribute default="Paste Identifier" check="String" primary="True">Paste Identifier</Attribute>
|
||||
<Attribute default="Paste Title" check="String" primary="False">Paste Title</Attribute>
|
||||
<Attribute default="Paste Source" check="String" primary="False">Paste Source</Attribute>
|
||||
<Attribute default="Paste ID" check="String" primary="False">Paste ID</Attribute>
|
||||
<Attribute default="0" check="Numbers" primary="False">Paste Email Count</Attribute>
|
||||
</Attributes>
|
||||
<Icon>
|
||||
Default.svg
|
||||
</Icon>
|
||||
</Paste_Data_Leak>
|
||||
</HIBP>
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class HIBPBreachToDomain:
|
||||
|
||||
name = "HIBP Breach To Domain"
|
||||
category = "Leaked Data"
|
||||
description = "Get the domain of the primary website that a data breach occurred on."
|
||||
originTypes = {'Data Breach'}
|
||||
resultTypes = {'Domain'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
|
||||
returnResults = []
|
||||
|
||||
for entity in entityJsonList:
|
||||
domainMaybe = entity.get('Breach Domain')
|
||||
if isinstance(domainMaybe, str):
|
||||
if domainMaybe.strip() != '':
|
||||
returnResults.append([{'Domain Name': domainMaybe,
|
||||
'Entity Type': 'Domain'},
|
||||
{entity['uid']: {'Resolution': 'Data Breach to Domain',
|
||||
'Notes': ''}}])
|
||||
|
||||
return returnResults
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class HaveIBeenPwnedBreachDomains:
|
||||
name = "HIBP Breach Domain Lookup"
|
||||
category = "Leaked Data"
|
||||
description = "Find breaches associated with a specified domain."
|
||||
originTypes = {'Domain'}
|
||||
resultTypes = {'Data Breach'}
|
||||
parameters = {'HIBP API Key': {'description': 'Enter your "Have I Been Pwned" API key. '
|
||||
'You can get a key here: https://haveibeenpwned.com/API/Key',
|
||||
'type': 'String',
|
||||
'value': '',
|
||||
'global': True,
|
||||
'default': 'None'}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import json
|
||||
from time import sleep
|
||||
|
||||
from PySide6.QtCore import QByteArray, QBuffer, QIODevice, QSize
|
||||
from PySide6.QtGui import QImage
|
||||
|
||||
baseURL = "https://haveibeenpwned.com/api/v3/breaches?domain="
|
||||
requestHeaders = {'hibp-api-key': parameters['HIBP API Key'].strip(), 'user-agent': 'LinkScope Client'}
|
||||
|
||||
returnResults = []
|
||||
|
||||
count = 0
|
||||
while count < len(entityJsonList):
|
||||
entity = entityJsonList[count]
|
||||
primaryField = entity['Domain Name']
|
||||
breachInfoRequest = requests.get(baseURL + primaryField, headers=requestHeaders)
|
||||
statusCode = breachInfoRequest.status_code
|
||||
if statusCode == 200:
|
||||
breachContent = json.loads(breachInfoRequest.content)
|
||||
|
||||
for breach in breachContent:
|
||||
try:
|
||||
breachLogoIconRequest = requests.get(breach['LogoPath'])
|
||||
breachIconByteArray = QByteArray(breachLogoIconRequest.content)
|
||||
breachIconImageOriginal = QImage().fromData(breachIconByteArray)
|
||||
breachIconImageScaled = breachIconImageOriginal.scaled(QSize(40, 40))
|
||||
|
||||
# Rotate the breach domain logo upside down
|
||||
breachIconImageRotated = breachIconImageScaled.mirrored()
|
||||
|
||||
breachIconByteArrayFin = QByteArray()
|
||||
breachImageBuffer = QBuffer(breachIconByteArrayFin)
|
||||
breachImageBuffer.open(QIODevice.WriteOnly)
|
||||
breachIconImageRotated.save(breachImageBuffer, "PNG")
|
||||
breachImageBuffer.close()
|
||||
except Exception:
|
||||
breachIconByteArrayFin = None
|
||||
|
||||
# If Breach Date is None, then default to entity creation date.
|
||||
returnResults.append([{'Breach Name': breach['Name'],
|
||||
'Breach Title': breach['Title'],
|
||||
'Breach Domain': breach['Domain'],
|
||||
'Breach Pwn Count': str(breach['PwnCount']),
|
||||
'Breach Description': breach['Description'],
|
||||
'Breach Is Sensitive': str(breach['IsSensitive']),
|
||||
'Breach Is Verified': str(breach['IsVerified']),
|
||||
'Breach Is Fabricated': str(breach['IsFabricated']),
|
||||
'Breach Is Retired': str(breach['IsRetired']),
|
||||
'Breach Is Spam List': str(breach['IsSpamList']),
|
||||
'Breach Is Malware': str(breach['IsMalware']),
|
||||
'Breach Added Date': breach['AddedDate'],
|
||||
'Breach Modified Date': breach['ModifiedDate'],
|
||||
'Entity Type': 'Data Breach',
|
||||
'Icon': breachIconByteArrayFin, # If None -> Default breach icon.
|
||||
'Date Created': breach['BreachDate']},
|
||||
{entity['uid']: {'Resolution': 'Contained in Breach',
|
||||
'Notes': ''}}])
|
||||
elif statusCode == 401:
|
||||
return "The HIBP API Key provided is invalid."
|
||||
elif statusCode == 429:
|
||||
sleep(2)
|
||||
continue
|
||||
elif statusCode == 503:
|
||||
return "The HIBP Service is unavailable."
|
||||
sleep(1.7)
|
||||
count += 1
|
||||
return returnResults
|
||||
@@ -1,88 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class HaveIBeenPwnedBreaches:
|
||||
name = "HIBP Breach Lookup"
|
||||
category = "Leaked Data"
|
||||
description = "Find all breaches that an account has been involved in. Note that Date Created for breaches is an " \
|
||||
"estimate."
|
||||
originTypes = {'Email Address', 'Phone Number'}
|
||||
resultTypes = {'Data Breach'}
|
||||
parameters = {'HIBP API Key': {'description': 'Enter your "Have I Been Pwned" API key. '
|
||||
'You can get a key here: https://haveibeenpwned.com/API/Key',
|
||||
'type': 'String',
|
||||
'value': '',
|
||||
'global': True,
|
||||
'default': 'None'}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import json
|
||||
from time import sleep
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from PySide6.QtCore import QByteArray, QBuffer, QIODevice, QSize
|
||||
from PySide6.QtGui import QImage
|
||||
|
||||
baseURL = "https://haveibeenpwned.com/api/v3/breachedaccount/"
|
||||
requestHeaders = {'hibp-api-key': parameters['HIBP API Key'].strip(), 'user-agent': 'LinkScope Client'}
|
||||
|
||||
returnResults = []
|
||||
|
||||
count = 0
|
||||
while count < len(entityJsonList):
|
||||
entity = entityJsonList[count]
|
||||
primaryField = entity[list(entity)[1]]
|
||||
breachInfoRequest = requests.get(baseURL + quote_plus(primaryField) + '?truncateResponse=false',
|
||||
headers=requestHeaders)
|
||||
statusCode = breachInfoRequest.status_code
|
||||
if statusCode == 200:
|
||||
breachContent = json.loads(breachInfoRequest.content)
|
||||
|
||||
for breach in breachContent:
|
||||
try:
|
||||
breachLogoIconRequest = requests.get(breach['LogoPath'])
|
||||
breachIconByteArray = QByteArray(breachLogoIconRequest.content)
|
||||
breachIconImageOriginal = QImage().fromData(breachIconByteArray)
|
||||
breachIconImageScaled = breachIconImageOriginal.scaled(QSize(40, 40))
|
||||
|
||||
# Rotate the breach domain logo upside down
|
||||
breachIconImageRotated = breachIconImageScaled.mirrored()
|
||||
|
||||
breachIconByteArrayFin = QByteArray()
|
||||
breachImageBuffer = QBuffer(breachIconByteArrayFin)
|
||||
breachImageBuffer.open(QIODevice.WriteOnly)
|
||||
breachIconImageRotated.save(breachImageBuffer, "PNG")
|
||||
breachImageBuffer.close()
|
||||
except Exception:
|
||||
breachIconByteArrayFin = None
|
||||
|
||||
# If Breach Date is None, then default to entity creation date.
|
||||
returnResults.append([{'Breach Name': breach['Name'],
|
||||
'Breach Title': breach['Title'],
|
||||
'Breach Domain': breach['Domain'],
|
||||
'Breach Pwn Count': str(breach['PwnCount']),
|
||||
'Breach Description': breach['Description'],
|
||||
'Breach Is Sensitive': str(breach['IsSensitive']),
|
||||
'Breach Is Verified': str(breach['IsVerified']),
|
||||
'Breach Is Fabricated': str(breach['IsFabricated']),
|
||||
'Breach Is Retired': str(breach['IsRetired']),
|
||||
'Breach Is Spam List': str(breach['IsSpamList']),
|
||||
'Breach Is Malware': str(breach['IsMalware']),
|
||||
'Breach Added Date': breach['AddedDate'],
|
||||
'Breach Modified Date': breach['ModifiedDate'],
|
||||
'Entity Type': 'Data Breach',
|
||||
'Icon': breachIconByteArrayFin, # If None -> Default breach icon.
|
||||
'Date Created': breach['BreachDate']},
|
||||
{entity['uid']: {'Resolution': 'Contained in Breach',
|
||||
'Notes': ''}}])
|
||||
elif statusCode == 401:
|
||||
return "The HIBP API Key provided is invalid."
|
||||
elif statusCode == 429:
|
||||
sleep(2)
|
||||
continue
|
||||
elif statusCode == 503:
|
||||
return "The HIBP Service is unavailable."
|
||||
sleep(1.7)
|
||||
count += 1
|
||||
return returnResults
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class HaveIBeenPwnedPassword:
|
||||
name = "HIBP Password Lookup"
|
||||
category = "Leaked Data"
|
||||
description = "Check whether the given password was found in breaches."
|
||||
originTypes = {'Phrase'}
|
||||
resultTypes = {'Phrase'}
|
||||
parameters = {'HIBP API Key': {'description': 'Enter your "Have I Been Pwned" API key. '
|
||||
'You can get a key here: https://haveibeenpwned.com/API/Key',
|
||||
'type': 'String',
|
||||
'value': '',
|
||||
'global': True,
|
||||
'default': 'None'}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
from time import sleep
|
||||
from hashlib import sha1
|
||||
|
||||
baseURL = "https://api.pwnedpasswords.com/range/"
|
||||
requestHeaders = {'hibp-api-key': parameters['HIBP API Key'].strip(), 'user-agent': 'LinkScope Client'}
|
||||
|
||||
returnResults = []
|
||||
|
||||
count = 0
|
||||
while count < len(entityJsonList):
|
||||
entity = entityJsonList[count]
|
||||
|
||||
primaryField = sha1(entity[list(entity)[1]].encode('utf-8')).hexdigest().upper()
|
||||
hashPrefix = primaryField[:5]
|
||||
hashSuffix = primaryField[5:]
|
||||
|
||||
breachInfoRequest = requests.get(baseURL + hashPrefix, headers=requestHeaders)
|
||||
statusCode = breachInfoRequest.status_code
|
||||
if statusCode == 200:
|
||||
pwnedPasswordContent = breachInfoRequest.content.decode('utf-8').split('\r\n')
|
||||
|
||||
for password in pwnedPasswordContent:
|
||||
if hashSuffix in password:
|
||||
returnResults.append([{'Phrase': f"Password Hash Found {password.split(':')[1]} times "
|
||||
f"in breach data.",
|
||||
'Entity Type': 'Phrase'},
|
||||
{entity['uid']: {'Resolution': 'Pwned Password',
|
||||
'Notes': ''}}])
|
||||
break
|
||||
elif statusCode == 401:
|
||||
return "The HIBP API Key provided is invalid."
|
||||
elif statusCode == 429:
|
||||
sleep(2)
|
||||
continue
|
||||
elif statusCode == 503:
|
||||
return "The HIBP Service is unavailable."
|
||||
sleep(1.7)
|
||||
count += 1
|
||||
return returnResults
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class HaveIBeenPwnedPasswordHash:
|
||||
name = "HIBP Password Hash Lookup"
|
||||
category = "Leaked Data"
|
||||
description = "Check whether the given password hash was found in breaches."
|
||||
originTypes = {'Hash', 'Phrase'}
|
||||
resultTypes = {'Phrase'}
|
||||
parameters = {'HIBP API Key': {'description': 'Enter your "Have I Been Pwned" API key. '
|
||||
'You can get a key here: https://haveibeenpwned.com/API/Key',
|
||||
'type': 'String',
|
||||
'value': '',
|
||||
'global': True,
|
||||
'default': 'None'}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
from time import sleep
|
||||
|
||||
baseURL = "https://api.pwnedpasswords.com/range/"
|
||||
requestHeaders = {'hibp-api-key': parameters['HIBP API Key'].strip(), 'user-agent': 'LinkScope Client'}
|
||||
|
||||
returnResults = []
|
||||
|
||||
count = 0
|
||||
while count < len(entityJsonList):
|
||||
entity = entityJsonList[count]
|
||||
|
||||
primaryField = entity[list(entity)[1]].upper()
|
||||
if len(primaryField) != 40:
|
||||
continue
|
||||
hashPrefix = primaryField[:5]
|
||||
hashSuffix = primaryField[5:]
|
||||
|
||||
breachInfoRequest = requests.get(baseURL + hashPrefix, headers=requestHeaders)
|
||||
statusCode = breachInfoRequest.status_code
|
||||
if statusCode == 200:
|
||||
pwnedPasswordContent = breachInfoRequest.content.decode('utf-8').split('\r\n')
|
||||
|
||||
for password in pwnedPasswordContent:
|
||||
if hashSuffix in password:
|
||||
returnResults.append([{'Phrase': f"Password Hash Found {password.split(':')[1]} times "
|
||||
f"in breach data.",
|
||||
'Entity Type': 'Phrase'},
|
||||
{entity['uid']: {'Resolution': 'Pwned Password',
|
||||
'Notes': ''}}])
|
||||
break
|
||||
elif statusCode == 401:
|
||||
return "The HIBP API Key provided is invalid."
|
||||
elif statusCode == 429:
|
||||
sleep(2)
|
||||
continue
|
||||
elif statusCode == 503:
|
||||
return "The HIBP Service is unavailable."
|
||||
sleep(1.7)
|
||||
count += 1
|
||||
return returnResults
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class HaveIBeenPwnedPastes:
|
||||
name = "HIBP Paste Lookup"
|
||||
category = "Leaked Data"
|
||||
description = "Find all pastes that an account has been involved in."
|
||||
originTypes = {'Email Address'}
|
||||
resultTypes = {'Paste Data Leak'}
|
||||
parameters = {'HIBP API Key': {'description': 'Enter your "Have I Been Pwned" API key. '
|
||||
'You can get a key here: https://haveibeenpwned.com/API/Key',
|
||||
'type': 'String',
|
||||
'value': '',
|
||||
'global': True,
|
||||
'default': 'None'}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import json
|
||||
from time import sleep
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
baseURL = "https://haveibeenpwned.com/api/v3/pasteaccount/"
|
||||
requestHeaders = {'hibp-api-key': parameters['HIBP API Key'].strip(), 'user-agent': 'LinkScope Client'}
|
||||
|
||||
returnResults = []
|
||||
|
||||
count = 0
|
||||
while count < len(entityJsonList):
|
||||
entity = entityJsonList[count]
|
||||
emailAddress = entity['Email Address']
|
||||
pasteInfoRequest = requests.get(baseURL + quote_plus(emailAddress), headers=requestHeaders)
|
||||
statusCode = pasteInfoRequest.status_code
|
||||
if statusCode == 200:
|
||||
pasteContent = json.loads(pasteInfoRequest.content)
|
||||
|
||||
for paste in pasteContent:
|
||||
pasteID = paste['Id']
|
||||
pasteSource = paste['Source']
|
||||
|
||||
# If Paste Date is None, then default to entity creation date.
|
||||
returnResults.append([{'Paste Identifier': f'{pasteSource} | {pasteID}',
|
||||
'Paste Title': paste['Title'],
|
||||
'Paste Source': pasteSource,
|
||||
'Paste ID': pasteID,
|
||||
'Paste Email Count': str(paste['EmailCount']),
|
||||
'Entity Type': 'Paste Data Leak',
|
||||
'Date Created': paste['Date']},
|
||||
{entity['uid']: {'Resolution': 'Contained in Paste',
|
||||
'Notes': ''}}])
|
||||
|
||||
elif statusCode == 401:
|
||||
return "The HIBP API Key provided is invalid."
|
||||
elif statusCode == 429:
|
||||
sleep(2)
|
||||
continue
|
||||
elif statusCode == 503:
|
||||
return "The HIBP Service is unavailable."
|
||||
sleep(1.7)
|
||||
count += 1
|
||||
return returnResults
|
||||
@@ -1 +0,0 @@
|
||||
requests
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class BlockChainAddressDestinations:
|
||||
|
||||
name = "Get Outbound Transactions for Bitcoin Address"
|
||||
category = "CryptoCurrency"
|
||||
description = "Returns the Bitcoin transactions where cryptocurrency was sent from this address."
|
||||
originTypes = {'BTC Address'}
|
||||
resultTypes = {'BTC Transaction'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
returnResults = []
|
||||
|
||||
apiEndpointAddress = 'https://blockchain.info/rawaddr/'
|
||||
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
primaryField = entity['BTC Address']
|
||||
|
||||
try:
|
||||
addressDetails = requests.get(apiEndpointAddress + primaryField).json()
|
||||
if addressDetails.get('error') is not None:
|
||||
continue
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
|
||||
blockTransactions = addressDetails.get('txs', [])
|
||||
|
||||
for transaction in blockTransactions:
|
||||
inputValue = 0
|
||||
isInAddr = False
|
||||
for transactionInput in transaction.get('inputs', []):
|
||||
inputValue += (int(transactionInput['prev_out']['value']) / 100000000)
|
||||
if transactionInput['prev_out']['addr'] == primaryField:
|
||||
isInAddr = True
|
||||
|
||||
if not isInAddr:
|
||||
continue
|
||||
|
||||
outputValue = 0
|
||||
for transactionOutput in transaction.get('out', []):
|
||||
outputValue += (int(transactionOutput['value']) / 100000000)
|
||||
|
||||
timestamp = datetime.utcfromtimestamp(transaction.get('time')).isoformat()
|
||||
|
||||
returnResults.append(
|
||||
[{'Transaction Hash': transaction['hash'],
|
||||
'Input Value (BTC)': str(inputValue),
|
||||
'Output Value (BTC)': str(outputValue),
|
||||
'Fee': str(transaction['fee']),
|
||||
'Number of Inputs': str(transaction['vin_sz']),
|
||||
'Number of Outputs': str(transaction['vout_sz']),
|
||||
'Transaction Index': str(transaction['tx_index']),
|
||||
'Size': str(transaction['size']),
|
||||
'Height': str(transaction['block_height']),
|
||||
'Entity Type': 'BTC Transaction',
|
||||
'Date Created': timestamp},
|
||||
{uid: {'Resolution': 'BTC Transaction',
|
||||
'Notes': ''}}])
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
return returnResults
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class BlockChainAddressSources:
|
||||
|
||||
name = "Get Inbound Transactions for Bitcoin Address"
|
||||
category = "CryptoCurrency"
|
||||
description = "Returns the Bitcoin transactions where cryptocurrency was sent to this address."
|
||||
originTypes = {'BTC Address'}
|
||||
resultTypes = {'BTC Transaction'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
returnResults = []
|
||||
returnResultResolutions = {}
|
||||
|
||||
apiEndpointAddress = 'https://blockchain.info/rawaddr/'
|
||||
|
||||
for entity in entityJsonList:
|
||||
primaryField = entity['BTC Address']
|
||||
|
||||
try:
|
||||
addressDetails = requests.get(apiEndpointAddress + primaryField).json()
|
||||
if addressDetails.get('error') is not None:
|
||||
continue
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
|
||||
blockTransactions = addressDetails.get('txs', [])
|
||||
|
||||
for transaction in blockTransactions:
|
||||
outputValue = 0
|
||||
isOutAddr = False
|
||||
for transactionOutput in transaction.get('out', []):
|
||||
outputValue += (int(transactionOutput['value']) / 100000000)
|
||||
if transactionOutput['addr'] == primaryField:
|
||||
isOutAddr = True
|
||||
|
||||
if not isOutAddr:
|
||||
continue
|
||||
|
||||
inputValue = 0
|
||||
for transactionInput in transaction.get('inputs', []):
|
||||
inputValue += (int(transactionInput['prev_out']['value']) / 100000000)
|
||||
|
||||
timestamp = datetime.utcfromtimestamp(transaction.get('time')).isoformat()
|
||||
returnResultResolutions[len(returnResults)] = {'Resolution': 'BTC Transaction'}
|
||||
|
||||
returnResults.append(
|
||||
[{'Transaction Hash': transaction['hash'],
|
||||
'Input Value (BTC)': str(inputValue),
|
||||
'Output Value (BTC)': str(outputValue),
|
||||
'Fee': str(transaction['fee']),
|
||||
'Number of Inputs': str(transaction['vin_sz']),
|
||||
'Number of Outputs': str(transaction['vout_sz']),
|
||||
'Transaction Index': str(transaction['tx_index']),
|
||||
'Size': str(transaction['size']),
|
||||
'Height': str(transaction['block_height']),
|
||||
'Entity Type': 'BTC Transaction',
|
||||
'Date Created': timestamp},
|
||||
{'^^^': {'Resolution': 'NULL',
|
||||
'Notes': ''}}])
|
||||
|
||||
time.sleep(5)
|
||||
returnResults.append([{'BTC Address': primaryField,
|
||||
'Entity Type': 'BTC Address'},
|
||||
returnResultResolutions])
|
||||
returnResultResolutions = {}
|
||||
return returnResults
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class BlockChainBlock:
|
||||
|
||||
name = "Get Bitcoin Block Details"
|
||||
category = "CryptoCurrency"
|
||||
description = "Returns the details of a particular bitcoin block."
|
||||
originTypes = {'Hash', 'Phrase', 'BTC Block'}
|
||||
resultTypes = {'BTC Block'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime
|
||||
returnResults = []
|
||||
|
||||
apiEndpoint = 'https://blockchain.info/rawblock/'
|
||||
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
primaryField = entity[list(entity)[1]]
|
||||
|
||||
try:
|
||||
details = requests.get(apiEndpoint + primaryField).json()
|
||||
if details.get('error') is not None:
|
||||
continue
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
|
||||
timestamp = datetime.utcfromtimestamp(details.get('time')).isoformat()
|
||||
|
||||
returnResults.append(
|
||||
[{'Block Address': details['hash'],
|
||||
'Previous Block': details['prev_block'],
|
||||
'Merkle Root': details['mrkl_root'],
|
||||
'Relayed By': details['relayed_by'],
|
||||
'Nonce': str(details['nonce']),
|
||||
'Bits': str(details['bits']),
|
||||
'Size': str(details['size']),
|
||||
'Block Index': str(details['block_index']),
|
||||
'Height': str(details['height']),
|
||||
'Main Chain': str(details['main_chain']),
|
||||
'Entity Type': 'BTC Block',
|
||||
'Date Created': timestamp},
|
||||
{uid: {'Resolution': 'Bitcoin Block Details',
|
||||
'Notes': ''}}])
|
||||
|
||||
time.sleep(5)
|
||||
return returnResults
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class BlockChainBlockHeight:
|
||||
|
||||
name = "Get Bitcoin Blocks At Height"
|
||||
category = "CryptoCurrency"
|
||||
description = "Returns the details of all bitcoin blocks at the specified height."
|
||||
originTypes = {'Phrase'}
|
||||
resultTypes = {'BTC Block'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime
|
||||
returnResults = []
|
||||
|
||||
apiEndpoint = 'https://blockchain.info/block-height/'
|
||||
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
try:
|
||||
primaryField = int(entity['Phrase'])
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
try:
|
||||
heightDetails = requests.get(apiEndpoint + str(primaryField)).json()
|
||||
if heightDetails.get('error') is not None:
|
||||
continue
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
|
||||
for details in heightDetails['blocks']:
|
||||
timestamp = datetime.utcfromtimestamp(details.get('time')).isoformat()
|
||||
|
||||
returnResults.append(
|
||||
[{'Block Address': details['hash'],
|
||||
'Previous Block': details['prev_block'],
|
||||
'Merkle Root': details['mrkl_root'],
|
||||
'Relayed By': details['relayed_by'],
|
||||
'Nonce': str(details['nonce']),
|
||||
'Bits': str(details['bits']),
|
||||
'Size': str(details['size']),
|
||||
'Block Index': str(details['block_index']),
|
||||
'Height': str(details['height']),
|
||||
'Main Chain': str(details['main_chain']),
|
||||
'Entity Type': 'BTC Block',
|
||||
'Date Created': timestamp},
|
||||
{uid: {'Resolution': 'Bitcoin Block Address',
|
||||
'Notes': ''}}])
|
||||
|
||||
time.sleep(5)
|
||||
return returnResults
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class BlockChainBlockNext:
|
||||
|
||||
name = "Get Next Bitcoin Block"
|
||||
category = "CryptoCurrency"
|
||||
description = "Returns the details of the next bitcoin block."
|
||||
originTypes = {'BTC Block'}
|
||||
resultTypes = {'BTC Block'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime
|
||||
returnResults = []
|
||||
|
||||
apiEndpoint = 'https://blockchain.info/rawblock/'
|
||||
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
primaryField = entity['Block Address']
|
||||
|
||||
try:
|
||||
currDetails = requests.get(apiEndpoint + primaryField).json()
|
||||
if currDetails.get('error') is not None:
|
||||
continue
|
||||
nextBlockHashList = currDetails.get('next_block')
|
||||
# Ignore nonexistent or indeterminate 'next' blocks.
|
||||
if nextBlockHashList is None or len(nextBlockHashList) > 1:
|
||||
continue
|
||||
time.sleep(5)
|
||||
details = requests.get(apiEndpoint + nextBlockHashList[0]).json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
|
||||
timestamp = datetime.utcfromtimestamp(details.get('time')).isoformat()
|
||||
|
||||
returnResults.append(
|
||||
[{'Block Address': details['hash'],
|
||||
'Previous Block': details['prev_block'],
|
||||
'Merkle Root': details['mrkl_root'],
|
||||
'Relayed By': details['relayed_by'],
|
||||
'Nonce': str(details['nonce']),
|
||||
'Bits': str(details['bits']),
|
||||
'Size': str(details['size']),
|
||||
'Block Index': str(details['block_index']),
|
||||
'Height': str(details['height']),
|
||||
'Main Chain': str(details['main_chain']),
|
||||
'Entity Type': 'BTC Block',
|
||||
'Date Created': timestamp},
|
||||
{uid: {'Resolution': 'Next BTC Block',
|
||||
'Notes': ''}}])
|
||||
|
||||
time.sleep(5)
|
||||
return returnResults
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class BlockChainBlockPrev:
|
||||
|
||||
name = "Get Previous Bitcoin Block"
|
||||
category = "CryptoCurrency"
|
||||
description = "Returns the details of the previous bitcoin block."
|
||||
originTypes = {'BTC Block'}
|
||||
resultTypes = {'BTC Block'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime
|
||||
returnResults = []
|
||||
|
||||
apiEndpoint = 'https://blockchain.info/rawblock/'
|
||||
|
||||
for entity in entityJsonList:
|
||||
primaryField = entity['Block Address']
|
||||
|
||||
try:
|
||||
currDetails = requests.get(apiEndpoint + primaryField).json()
|
||||
if currDetails.get('error') is not None:
|
||||
continue
|
||||
prevBlockHash = currDetails.get('prev_block')
|
||||
# Ignore first block.
|
||||
if prevBlockHash == "0000000000000000000000000000000000000000000000000000000000000000":
|
||||
continue
|
||||
time.sleep(5)
|
||||
details = requests.get(apiEndpoint + prevBlockHash).json()
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
|
||||
timestamp = datetime.utcfromtimestamp(details.get('time')).isoformat()
|
||||
|
||||
returnResults.append(
|
||||
[{'Block Address': details['hash'],
|
||||
'Previous Block': details['prev_block'],
|
||||
'Merkle Root': details['mrkl_root'],
|
||||
'Relayed By': details['relayed_by'],
|
||||
'Nonce': str(details['nonce']),
|
||||
'Bits': str(details['bits']),
|
||||
'Size': str(details['size']),
|
||||
'Block Index': str(details['block_index']),
|
||||
'Height': str(details['height']),
|
||||
'Main Chain': str(details['main_chain']),
|
||||
'Entity Type': 'BTC Block',
|
||||
'Date Created': timestamp},
|
||||
{'^^^': {'Resolution': 'NULL'}}])
|
||||
|
||||
returnResults.append([{'Block Address': primaryField,
|
||||
'Entity Type': 'BTC Block'},
|
||||
{len(returnResults) - 1: {'Resolution': 'Next BTC Block'}}])
|
||||
|
||||
time.sleep(5)
|
||||
return returnResults
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class BlockChainBlockTransactions:
|
||||
|
||||
name = "Get Bitcoin Block Transactions"
|
||||
category = "CryptoCurrency"
|
||||
description = "Returns the transactions that happened in a particular bitcoin block."
|
||||
originTypes = {'BTC Block'}
|
||||
resultTypes = {'BTC Transaction'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime
|
||||
returnResults = []
|
||||
|
||||
apiEndpoint = 'https://blockchain.info/rawblock/'
|
||||
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
primaryField = entity['Block Address']
|
||||
|
||||
try:
|
||||
details = requests.get(apiEndpoint + primaryField).json()
|
||||
if details.get('error') is not None:
|
||||
continue
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
|
||||
blockTransactions = details.get('tx', [])
|
||||
|
||||
for transaction in blockTransactions:
|
||||
inputValue = 0
|
||||
for transactionInput in transaction.get('inputs', []):
|
||||
inputValue += (int(transactionInput['prev_out']['value']) / 100000000)
|
||||
|
||||
outputValue = 0
|
||||
for transactionOutput in transaction.get('out', []):
|
||||
outputValue += (int(transactionOutput['value']) / 100000000)
|
||||
|
||||
timestamp = datetime.utcfromtimestamp(transaction.get('time')).isoformat()
|
||||
|
||||
returnResults.append(
|
||||
[{'Transaction Hash': transaction['hash'],
|
||||
'Input Value (BTC)': str(inputValue),
|
||||
'Output Value (BTC)': str(outputValue),
|
||||
'Fee': str(transaction['fee']),
|
||||
'Number of Inputs': str(transaction['vin_sz']),
|
||||
'Number of Outputs': str(transaction['vout_sz']),
|
||||
'Transaction Index': str(transaction['tx_index']),
|
||||
'Size': str(transaction['size']),
|
||||
'Height': str(transaction['block_height']),
|
||||
'Entity Type': 'BTC Transaction',
|
||||
'Date Created': timestamp},
|
||||
{uid: {'Resolution': 'Bitcoin Block Address',
|
||||
'Notes': ''}}])
|
||||
|
||||
time.sleep(5)
|
||||
return returnResults
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class BlockChainTransaction:
|
||||
|
||||
name = "Get Bitcoin Transaction"
|
||||
category = "CryptoCurrency"
|
||||
description = "Returns the details of the specified Bitcoin transaction."
|
||||
originTypes = {'BTC Transaction', 'Hash', 'Phrase'}
|
||||
resultTypes = {'BTC Transaction'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime
|
||||
returnResults = []
|
||||
|
||||
apiEndpoint = 'https://blockchain.info/rawtx/'
|
||||
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
primaryField = entity[list(entity)[1]]
|
||||
|
||||
try:
|
||||
transaction = requests.get(apiEndpoint + primaryField).json()
|
||||
if transaction.get('error') is not None:
|
||||
continue
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
|
||||
timestamp = datetime.utcfromtimestamp(transaction.get('time')).isoformat()
|
||||
|
||||
inputValue = 0
|
||||
for transactionInput in transaction.get('inputs', []):
|
||||
inputValue += (int(transactionInput['prev_out']['value']) / 100000000)
|
||||
|
||||
outputValue = 0
|
||||
for transactionOutput in transaction.get('out', []):
|
||||
outputValue += (int(transactionOutput['value']) / 100000000)
|
||||
|
||||
returnResults.append(
|
||||
[{'Transaction Hash': transaction['hash'],
|
||||
'Input Value (BTC)': str(inputValue),
|
||||
'Output Value (BTC)': str(outputValue),
|
||||
'Fee': str(transaction['fee']),
|
||||
'Number of Inputs': str(transaction['vin_sz']),
|
||||
'Number of Outputs': str(transaction['vout_sz']),
|
||||
'Transaction Index': str(transaction['tx_index']),
|
||||
'Size': str(transaction['size']),
|
||||
'Height': str(transaction['block_height']),
|
||||
'Entity Type': 'BTC Transaction',
|
||||
'Date Created': timestamp},
|
||||
{uid: {'Resolution': 'Bitcoin Transaction Information',
|
||||
'Notes': ''}}])
|
||||
|
||||
time.sleep(5)
|
||||
return returnResults
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class BlockChainTransactionDestinations:
|
||||
|
||||
name = "Get Bitcoin Transaction Destinations"
|
||||
category = "CryptoCurrency"
|
||||
description = "Returns the Bitcoin addresses that received cryptocurrency in the specified transaction."
|
||||
originTypes = {'BTC Transaction'}
|
||||
resultTypes = {'BTC Address'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import time
|
||||
|
||||
returnResults = []
|
||||
|
||||
apiEndpointTransaction = 'https://blockchain.info/rawtx/'
|
||||
apiEndpointAddress = 'https://blockchain.info/rawaddr/'
|
||||
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
primaryField = entity['Transaction Hash']
|
||||
|
||||
try:
|
||||
transaction = requests.get(apiEndpointTransaction + primaryField).json()
|
||||
if transaction.get('error') is not None:
|
||||
continue
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
|
||||
for transactionInput in transaction.get('out', []):
|
||||
time.sleep(5)
|
||||
inputAddress = transactionInput['addr']
|
||||
try:
|
||||
details = requests.get(apiEndpointAddress + inputAddress).json()
|
||||
if details.get('error') is not None:
|
||||
continue
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
|
||||
returnResults.append(
|
||||
[{'BTC Address': details['address'],
|
||||
'Total Transactions': str(details['n_tx']),
|
||||
'Unredeemed Transactions': str(details['n_unredeemed']),
|
||||
'Total BTC Received': str(details['total_received'] / 100000000),
|
||||
'Total BTC Sent': str(details['total_sent'] / 100000000),
|
||||
'Current Balance': str(details['final_balance'] / 100000000),
|
||||
'Entity Type': 'BTC Address'},
|
||||
{uid: {'Resolution': 'Bitcoin Transaction',
|
||||
'Notes': ''}}])
|
||||
|
||||
time.sleep(5)
|
||||
return returnResults
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class BlockChainTransactionSources:
|
||||
|
||||
name = "Get Bitcoin Transaction Sources"
|
||||
category = "CryptoCurrency"
|
||||
description = "Returns the Bitcoin addresses that sent cryptocurrency in the specified transaction."
|
||||
originTypes = {'BTC Transaction'}
|
||||
resultTypes = {'BTC Address'}
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import time
|
||||
|
||||
returnResults = []
|
||||
returnResultResolutions = {}
|
||||
|
||||
apiEndpointTransaction = 'https://blockchain.info/rawtx/'
|
||||
apiEndpointAddress = 'https://blockchain.info/rawaddr/'
|
||||
|
||||
for entity in entityJsonList:
|
||||
primaryField = entity['Transaction Hash']
|
||||
|
||||
try:
|
||||
transaction = requests.get(apiEndpointTransaction + primaryField).json()
|
||||
if transaction.get('error') is not None:
|
||||
continue
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
|
||||
for transactionInput in transaction.get('inputs', []):
|
||||
time.sleep(5)
|
||||
inputAddress = transactionInput['prev_out']['addr']
|
||||
try:
|
||||
details = requests.get(apiEndpointAddress + inputAddress).json()
|
||||
if details.get('error') is not None:
|
||||
continue
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
|
||||
returnResultResolutions[len(returnResults)] = {'Resolution': 'BTC Transaction'}
|
||||
returnResults.append(
|
||||
[{'BTC Address': details['address'],
|
||||
'Total Transactions': str(details['n_tx']),
|
||||
'Unredeemed Transactions': str(details['n_unredeemed']),
|
||||
'Total BTC Received': str(details['total_received'] / 100000000),
|
||||
'Total BTC Sent': str(details['total_sent'] / 100000000),
|
||||
'Current Balance': str(details['final_balance'] / 100000000),
|
||||
'Entity Type': 'BTC Address'},
|
||||
{'^^^': {'Resolution': 'NULL',
|
||||
'Notes': ''}}])
|
||||
|
||||
time.sleep(5)
|
||||
# Re-add the source entity so that we can point to it.
|
||||
# Only include the primary field, in case the rest of the fields were updated in the meantime.
|
||||
returnResults.append([{'Transaction Hash': primaryField,
|
||||
'Entity Type': 'BTC Transaction'},
|
||||
returnResultResolutions])
|
||||
returnResultResolutions = {}
|
||||
return returnResults
|
||||
@@ -1,26 +0,0 @@
|
||||
<CryptoCurrency>
|
||||
<BTC_Block>
|
||||
<Attributes>
|
||||
<Attribute default="0000000000000000000000000000000000000000000000000000000000000000" check="String" primary="True">Block Address</Attribute>
|
||||
</Attributes>
|
||||
<Icon>
|
||||
Bitcoin.svg
|
||||
</Icon>
|
||||
</BTC_Block>
|
||||
<BTC_Transaction>
|
||||
<Attributes>
|
||||
<Attribute default="0000000000000000000000000000000000000000000000000000000000000000" check="String" primary="True">Transaction Hash</Attribute>
|
||||
</Attributes>
|
||||
<Icon>
|
||||
Transaction.svg
|
||||
</Icon>
|
||||
</BTC_Transaction>
|
||||
<BTC_Address>
|
||||
<Attributes>
|
||||
<Attribute default="1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" check="String" primary="True">BTC Address</Attribute>
|
||||
</Attributes>
|
||||
<Icon>
|
||||
CryptoWallet.svg
|
||||
</Icon>
|
||||
</BTC_Address>
|
||||
</CryptoCurrency>
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class EtherScanGetBalance:
|
||||
name = "EtherScan.io Get Balance"
|
||||
category = "CryptoCurrency"
|
||||
description = "EtherScan get the balance of the selected account"
|
||||
originTypes = {"Crypto Wallet"}
|
||||
resultTypes = {'Crypto Wallet'}
|
||||
parameters = {'EtherScan API Key': {'description': "Enter the api key under your profile after signing up at "
|
||||
"https://etherscan.io.",
|
||||
'type': 'String',
|
||||
'value': '',
|
||||
'global': True}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import time
|
||||
|
||||
return_result = []
|
||||
|
||||
api_key = parameters['EtherScan API Key']
|
||||
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
primary_field = entity['Wallet Address']
|
||||
crafted_url = f"https://api.etherscan.io/api?module=account&action=balance" \
|
||||
f"&address={primary_field}&tag=latest&apikey={api_key}"
|
||||
try:
|
||||
response = requests.get(crafted_url)
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
response = response.json()
|
||||
return_result.append([{'Amount': response['result'],
|
||||
'Currency': 'Ethereum',
|
||||
'Entity Type': 'Currency'},
|
||||
{uid: {'Resolution': 'EtherScan.io Account Balance', 'Notes': ''}}])
|
||||
time.sleep(0.2)
|
||||
|
||||
return return_result
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class EtherScanGetBlocksMined:
|
||||
name = "EtherScan.io Get Blocks Mined"
|
||||
category = "CryptoCurrency"
|
||||
description = "EtherScan Blocks Mined from the selected account"
|
||||
originTypes = {"Crypto Wallet"}
|
||||
resultTypes = {'Crypto Wallet'}
|
||||
parameters = {'EtherScan API Key': {'description': "Enter the api key under your profile after signing up at "
|
||||
"https://etherscan.io.",
|
||||
'type': 'String',
|
||||
'value': '',
|
||||
'global': True}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import time
|
||||
|
||||
return_result = []
|
||||
|
||||
api_key = parameters['EtherScan API Key']
|
||||
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
primary_field = entity['Wallet Address']
|
||||
crafted_url = f"https://api.etherscan.io/api?module=account&action=getminedblocks" \
|
||||
f"&address={primary_field}&tag=latest&apikey={api_key}"
|
||||
try:
|
||||
response = requests.get(crafted_url)
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "Please check your internet connection"
|
||||
response = response.json()
|
||||
return_result.append([{'Phrase': response['result'],
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': 'EtherScan.io Blocks Mined', 'Notes': ''}}])
|
||||
time.sleep(0.2)
|
||||
|
||||
return return_result
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class ToCryptoWallet:
|
||||
name = "BTC Address To Crypto Wallet"
|
||||
category = "CryptoCurrency"
|
||||
description = "Convert BTC Address entities to Crypto Wallet entities."
|
||||
originTypes = {'BTC Address'}
|
||||
resultTypes = {'Crypto Wallet'}
|
||||
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
|
||||
returnResults = []
|
||||
|
||||
for entity in entityJsonList:
|
||||
primaryField = entity['BTC Address']
|
||||
returnResults.append([{'Wallet Address': primaryField,
|
||||
'Currency Name': 'Bitcoin',
|
||||
'Entity Type': 'Crypto Wallet'},
|
||||
{entity['uid']: {'Resolution': 'To Crypto Wallet',
|
||||
'Notes': ''}}])
|
||||
|
||||
return returnResults
|
||||
@@ -1,2 +0,0 @@
|
||||
requests
|
||||
datetime
|
||||
@@ -1,107 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
class CompanyInfo:
|
||||
# A string that is treated as the name of this resolution.
|
||||
name = "Get Company Info"
|
||||
|
||||
category = "EDGAR Info"
|
||||
|
||||
# A string that describes this resolution.
|
||||
description = "Returns Nodes containing Company Information"
|
||||
|
||||
originTypes = {'Edgar ID'}
|
||||
|
||||
resultTypes = {'Phrase, SIC, EIN, Address'}
|
||||
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
import time
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
}
|
||||
|
||||
returnResults = []
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
cik = entity['CIK']
|
||||
if cik.lower().startswith('cik'):
|
||||
cik = cik.split('cik')[1]
|
||||
if len(cik) != 10:
|
||||
cik = cik.zfill(1)
|
||||
search_url = f'https://data.sec.gov/submissions/CIK{cik}.json'
|
||||
time.sleep(1)
|
||||
r = requests.get(search_url, headers=headers)
|
||||
time.sleep(1)
|
||||
if r.status_code != 200:
|
||||
return []
|
||||
|
||||
data = r.json()
|
||||
|
||||
exchanges = data['exchanges']
|
||||
for exchange in exchanges:
|
||||
returnResults.append([{'Exchange Name': exchange,
|
||||
'Entity Type': 'Exchange'},
|
||||
{uid: {'Resolution': 'Exchange',
|
||||
'Notes': ''}}])
|
||||
|
||||
tickers = data['tickers']
|
||||
for ticker in tickers:
|
||||
returnResults.append([{'Ticker ID': ticker,
|
||||
'Entity Type': 'Ticker'},
|
||||
{uid: {'Resolution': 'Ticker',
|
||||
'Notes': ''}}])
|
||||
|
||||
if data['insiderTransactionForOwnerExists'] == 1:
|
||||
returnResults.append([{'Phrase': 'Insider Transaction For Owner Exists',
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': '',
|
||||
'Notes': ''}}])
|
||||
else:
|
||||
returnResults.append([{'Phrase': 'Insider Transaction For Owner Does Not Exists',
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': '',
|
||||
'Notes': ''}}])
|
||||
|
||||
if data['insiderTransactionForIssuerExists'] == 1:
|
||||
returnResults.append([{'Phrase': 'Insider Transaction For Issuer Exists',
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': '',
|
||||
'Notes': ''}}])
|
||||
else:
|
||||
returnResults.append([{'Phrase': 'Insider Transaction For Issuer Does Not Exists',
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': '',
|
||||
'Notes': ''}}])
|
||||
|
||||
if data['sic'] is not None:
|
||||
returnResults.append([{'SIC': str(data['sic']),
|
||||
'Description': data['sicDescription'],
|
||||
'Entity Type': 'SIC'},
|
||||
{uid: {'Resolution': '',
|
||||
'Notes': ''}}])
|
||||
if data['ein'] is not None:
|
||||
returnResults.append([{'EIN': str(data['ein']),
|
||||
'Entity Type': 'EIN'},
|
||||
{uid: {'Resolution': '',
|
||||
'Notes': ''}}])
|
||||
if data['addresses'] is not None:
|
||||
returnResults.append([{'Street Address': data['addresses']['mailing']['street1'],
|
||||
'Postal Code': data['addresses']['mailing']['zipCode'],
|
||||
'Country': data['addresses']['mailing']['stateOrCountry'],
|
||||
'Locality': data['addresses']['mailing']['city'],
|
||||
'Entity Type': 'Address'},
|
||||
{uid: {'Resolution': '',
|
||||
'Notes': ''}}])
|
||||
|
||||
if data['addresses']['mailing']['street1'] != data['addresses']['business']['street1']:
|
||||
returnResults.append([{'Street Address': data['addresses']['business']['street1'],
|
||||
'Postal Code': data['addresses']['business']['zipCode'],
|
||||
'Country': data['addresses']['business']['stateOrCountry'],
|
||||
'Locality': data['addresses']['business']['city'],
|
||||
'Entity Type': 'Address'},
|
||||
{uid: {'Resolution': '',
|
||||
'Notes': ''}}])
|
||||
return returnResults
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class CompanyToCIK:
|
||||
# A string that is treated as the name of this resolution.
|
||||
name = "Get CIK ID From Company"
|
||||
|
||||
category = "EDGAR Info"
|
||||
|
||||
# A string that describes this resolution.
|
||||
description = "Returns Nodes of contact info for websites"
|
||||
|
||||
originTypes = {'Phrase', 'Company'}
|
||||
|
||||
resultTypes = {'Phrase'}
|
||||
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
from bs4 import BeautifulSoup
|
||||
from playwright.sync_api import sync_playwright, TimeoutError, Error
|
||||
|
||||
returnResults = []
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.firefox.launch()
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080}
|
||||
)
|
||||
page = context.new_page()
|
||||
for entity in entityJsonList:
|
||||
page.wait_for_timeout(1000)
|
||||
uid = entity['uid']
|
||||
search_term = entity[list(entity)[1]]
|
||||
pageResolved = False
|
||||
for _ in range(3):
|
||||
try:
|
||||
page.goto(f'https://www.sec.gov/cgi-bin/browse-edgar?company={search_term}',
|
||||
wait_until="networkidle", timeout=10000)
|
||||
pageResolved = True
|
||||
break
|
||||
except TimeoutError:
|
||||
pass
|
||||
except Error:
|
||||
break
|
||||
if not pageResolved:
|
||||
continue
|
||||
|
||||
soup = BeautifulSoup(page.content(), 'lxml')
|
||||
|
||||
count = 0
|
||||
temp = None
|
||||
for td_element in soup.find_all('td'):
|
||||
if td_element.text:
|
||||
text = td_element.text
|
||||
splitText = text.split('SIC')[0]
|
||||
if count == 0:
|
||||
temp = [{'CIK': splitText,
|
||||
'Entity Type': 'Edgar ID'},
|
||||
{len(returnResults): {'Resolution': 'CIK Edgar ID',
|
||||
'Notes': ''}}]
|
||||
elif count == 1:
|
||||
returnResults.append([{'Company Name': splitText,
|
||||
'Entity Type': 'Company'},
|
||||
{uid: {'Resolution': 'Edgar Company',
|
||||
'Notes': ''}}])
|
||||
returnResults.append(temp)
|
||||
elif count == 2:
|
||||
returnResults.append([{'Phrase': "State: " + splitText,
|
||||
'Entity Type': 'Phrase'},
|
||||
{len(returnResults) - 1: {'Resolution': 'Edgar Company State',
|
||||
'Notes': ''}}])
|
||||
count = (count + 1) % 3
|
||||
|
||||
page.close()
|
||||
browser.close()
|
||||
return returnResults
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user