Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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>
|
||||
|
||||
@@ -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)
|
||||
@@ -379,9 +379,11 @@ 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)
|
||||
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:
|
||||
|
||||
@@ -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 '
|
||||
|
||||
@@ -20,8 +20,9 @@ from PySide6.QtWidgets import QGraphicsPixmapItem
|
||||
from PySide6.QtSvgWidgets import QGraphicsSvgItem
|
||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||
|
||||
from Core.Interface import Entity, Stylesheets
|
||||
from Core.Interface import Entity
|
||||
from Core.ResourceHandler import RichNotesEditor
|
||||
from Core.GlobalVariables import hidden_fields
|
||||
|
||||
|
||||
class WorkspaceWidget(QtWidgets.QWidget):
|
||||
@@ -90,7 +91,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 +124,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')
|
||||
|
||||
@@ -193,6 +193,9 @@ class TabbedPane(QtWidgets.QTabWidget):
|
||||
self.tabsNotesDict = {}
|
||||
self.previousTab = None
|
||||
|
||||
self.allBanners = {bannerID: bannerPath
|
||||
for bannerID, bannerPath in self.mainWindow.RESOURCEHANDLER.banners.items()}
|
||||
|
||||
self.currentChanged.connect(self.currentTabChangedListener)
|
||||
|
||||
def getCanvasDBPath(self):
|
||||
@@ -352,7 +355,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 +603,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 +750,26 @@ 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 +834,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:
|
||||
@@ -915,8 +926,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 +1008,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 +1037,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 +1049,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 +1077,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 +1157,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 len(selectedEntities) < 1:
|
||||
self.tabbedPane.mainWindow.MESSAGEHANDLER.warning('Need to select at least one Entity to set its banner.',
|
||||
popUp=True)
|
||||
return
|
||||
bannerDialog = BannerSelector(self.tabbedPane.allBanners)
|
||||
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 +1193,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 +1206,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 +1273,48 @@ 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().allBanners[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)
|
||||
bannerPathStr = self.parent().allBanners.get(entityJson.get('Canvas Banner', ''), '')
|
||||
if not bannerPathStr:
|
||||
entity.updateBanner(True, None)
|
||||
else:
|
||||
with open(bannerPathStr, 'rb') as bannerFile:
|
||||
bannerByteArray = QtCore.QByteArray(bannerFile.read())
|
||||
entity.updateBanner(False, bannerByteArray)
|
||||
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 +1337,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 +1457,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:
|
||||
@@ -1515,7 +1607,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.
|
||||
@@ -1795,13 +1887,13 @@ class CanvasScene(QtWidgets.QGraphicsScene):
|
||||
self.removeItem(item.iconItem)
|
||||
|
||||
pictureByteArray = pEditor.objectJson['Icon']
|
||||
item.pixmapItem = QtGui.QPixmap()
|
||||
item.pixmapItem.loadFromData(pictureByteArray)
|
||||
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())
|
||||
@@ -1971,14 +2063,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 +2082,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 +2094,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 +2113,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 +2142,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()))
|
||||
|
||||
@@ -2075,7 +2166,7 @@ class PropertiesEditorIconField(QtWidgets.QLabel):
|
||||
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:
|
||||
@@ -2099,7 +2190,7 @@ class PropertiesEditorIconField(QtWidgets.QLabel):
|
||||
self.pictureByteArray = QtCore.QByteArray()
|
||||
imageBuffer = QtCore.QBuffer(self.pictureByteArray)
|
||||
|
||||
imageBuffer.open(QtCore.QIODevice.WriteOnly)
|
||||
imageBuffer.open(QtCore.QIODevice.OpenModeFlag.WriteOnly)
|
||||
|
||||
thumbnail.save(imageBuffer, "PNG")
|
||||
imageBuffer.close()
|
||||
@@ -2119,7 +2210,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 +2233,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,7 +1,6 @@
|
||||
#!/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
|
||||
|
||||
|
||||
@@ -36,11 +35,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 +72,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 +93,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()
|
||||
@@ -178,7 +174,12 @@ class EntityList(QtWidgets.QTreeWidget):
|
||||
"""
|
||||
Handle dragging of entities onto canvas.
|
||||
"""
|
||||
itemDragged = self.itemAt(event.pos())
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
if event.buttons() == QtCore.Qt.MouseButton.LeftButton:
|
||||
itemDragged = self.itemAt(event.pos())
|
||||
else:
|
||||
return
|
||||
|
||||
# Categories & entity names don't have uids.
|
||||
try:
|
||||
@@ -201,7 +202,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 +244,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 +300,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 +308,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 +366,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 +395,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 +415,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.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,7 +128,7 @@ 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("")
|
||||
@@ -164,13 +164,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 +273,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)
|
||||
@@ -304,7 +301,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)
|
||||
|
||||
@@ -415,10 +414,10 @@ class SingleLinkItem(QtWidgets.QWidget):
|
||||
|
||||
self.linkItemPic = QtWidgets.QLabel()
|
||||
|
||||
self.linkItemPic.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.linkItemPic.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.linkItemName = QtWidgets.QLabel()
|
||||
|
||||
self.linkItemName.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.linkItemName.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.linkItemUid = ""
|
||||
self.setMaximumHeight(90)
|
||||
|
||||
@@ -441,7 +440,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 +472,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 +539,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 +549,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.")
|
||||
|
||||
@@ -20,10 +20,7 @@ class BaseNode(QGraphicsItemGroup):
|
||||
brush: QtGui.QBrush) -> None:
|
||||
super(BaseNode, self).__init__()
|
||||
|
||||
self.setCacheMode(self.DeviceCoordinateCache)
|
||||
|
||||
self.pixmapItem = QtGui.QPixmap()
|
||||
self.pixmapItem.loadFromData(pictureByteArray)
|
||||
self.setCacheMode(QGraphicsItemGroup.CacheMode.DeviceCoordinateCache)
|
||||
|
||||
if pictureByteArray.data().startswith(b'<svg '):
|
||||
self.iconItem = QGraphicsSvgItem()
|
||||
@@ -32,20 +29,26 @@ class BaseNode(QGraphicsItemGroup):
|
||||
# https://stackoverflow.com/a/68182093
|
||||
self.iconItem.setElementId("")
|
||||
else:
|
||||
self.iconItem = QGraphicsPixmapItem(self.pixmapItem)
|
||||
pixmapItem = QtGui.QPixmap()
|
||||
pixmapItem.loadFromData(pictureByteArray)
|
||||
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 +56,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 +69,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 +129,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 +162,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:
|
||||
@@ -185,7 +202,7 @@ 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)
|
||||
|
||||
def addItemToGroup(self, uid: str) -> None:
|
||||
self.groupedNodesUid.add(uid)
|
||||
@@ -250,8 +267,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()
|
||||
@@ -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)
|
||||
|
||||
@@ -27,14 +27,13 @@ from playwright.sync_api import sync_playwright, Error, TimeoutError
|
||||
|
||||
from PySide6 import QtWidgets, QtGui, QtCore
|
||||
from Core.GlobalVariables import user_agents
|
||||
from Core.Interface import Stylesheets
|
||||
from Core.Interface.Entity import BaseNode
|
||||
from Core.ResourceHandler import StringPropertyInput, FilePropertyInput, SingleChoicePropertyInput, \
|
||||
MultiChoicePropertyInput
|
||||
|
||||
|
||||
class MenuBar(QtWidgets.QMenuBar):
|
||||
browserTabsImportDoneSignalListener = QtCore.Signal(list, bool, str)
|
||||
browserTabsImportDoneSignalListener = QtCore.Signal(list, str)
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent=parent)
|
||||
@@ -42,7 +41,6 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
self.browserTabsImportDoneSignalListener.connect(self.importBrowserTabsFindings)
|
||||
|
||||
fileMenu = self.addMenu("File")
|
||||
fileMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
|
||||
saveAction = QtGui.QAction("&Save",
|
||||
self,
|
||||
@@ -64,13 +62,17 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
fileMenu.addAction(renameAction)
|
||||
|
||||
importMenu = self.addMenu("Import")
|
||||
importMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
|
||||
fromBrowserAction = QtGui.QAction("From Browser",
|
||||
self,
|
||||
statusTip="Import open tabs as Website and materials entities.",
|
||||
triggered=self.importFromBrowser)
|
||||
|
||||
fromTorBrowserAction = QtGui.QAction("From TOR Browser",
|
||||
self,
|
||||
statusTip="Import open TOR tabs as Website and materials entities.",
|
||||
triggered=self.importFromTORBrowser)
|
||||
|
||||
fromFileAction = QtGui.QAction("From File",
|
||||
self,
|
||||
statusTip="Import entities from a file.",
|
||||
@@ -86,12 +88,12 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
statusTip="Import database from a GraphML file.",
|
||||
triggered=self.parent().importDatabaseFromGraphML)
|
||||
importMenu.addAction(fromBrowserAction)
|
||||
importMenu.addAction(fromTorBrowserAction)
|
||||
importMenu.addAction(fromFileAction)
|
||||
importMenu.addAction(graphMLCanvasAction)
|
||||
importMenu.addAction(graphMLDatabaseAction)
|
||||
|
||||
exportMenu = self.addMenu("Export")
|
||||
exportMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
|
||||
canvasPictureAction = QtGui.QAction("Save Picture of Canvas", self,
|
||||
statusTip="Save a picture of your canvas",
|
||||
@@ -148,7 +150,6 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
fileMenu.addAction(exitAction)
|
||||
|
||||
viewMenu = self.addMenu("View")
|
||||
viewMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
|
||||
findAction = QtGui.QAction("&Find",
|
||||
self,
|
||||
@@ -237,7 +238,6 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
toolbarVisibilityMenu.addAction(self.primaryToolbarVisibilityAction)
|
||||
|
||||
nodeOperationsMenu = self.addMenu("Node Operations")
|
||||
nodeOperationsMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
|
||||
actionSelectAllNodes = QtGui.QAction('Select All Nodes',
|
||||
self,
|
||||
@@ -339,7 +339,6 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
nodeOperationsMenu.addAction(detectCyclesAction)
|
||||
|
||||
projectMenu = self.addMenu("Project Operations")
|
||||
projectMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
generateReportAction = QtGui.QAction("Generate Report",
|
||||
self,
|
||||
statusTip="Generate a report from the set of currently selected nodes.",
|
||||
@@ -353,7 +352,6 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
projectMenu.addAction(queryAction)
|
||||
|
||||
modulesMenu = self.addMenu("Modules")
|
||||
modulesMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
|
||||
reloadModulesAction = QtGui.QAction("Reload Modules", self,
|
||||
statusTip="Reload all Entities and Transforms from Modules",
|
||||
@@ -361,7 +359,6 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
modulesMenu.addAction(reloadModulesAction)
|
||||
|
||||
serverMenu = self.addMenu("&Server")
|
||||
serverMenu.setStyleSheet(Stylesheets.MENUS_STYLESHEET_2)
|
||||
|
||||
connectAction = QtGui.QAction("Connect", self,
|
||||
statusTip="Connect to a Server",
|
||||
@@ -541,22 +538,26 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
self.parent().LENTDB.addEntity(existingEntity, updateTimeline=False)
|
||||
|
||||
elif importDialog.CSVFileChoice.isChecked():
|
||||
csvDF = pd.read_csv(fileDirectory)
|
||||
try:
|
||||
csvDF = pd.read_excel(fileDirectory)
|
||||
except ValueError:
|
||||
csvDF = pd.read_csv(fileDirectory)
|
||||
|
||||
# If we have no rows or columns, we cannot import anything.
|
||||
# This is essentially a sanity check.
|
||||
if len(csvDF.index) < 1:
|
||||
raise ValueError("Invalid import file data - Not enough rows.")
|
||||
if len(csvDF.columns) < 1:
|
||||
raise ValueError("Invalid import file data - Not enough columns.")
|
||||
|
||||
# Remove duplicate column names
|
||||
# When importing, all columns should be given unique values,
|
||||
# so this should be just another sanity check.
|
||||
csvDF = csvDF.loc[:, ~csvDF.columns.duplicated()]
|
||||
|
||||
# Fill NaN values with an empty string
|
||||
csvDF.fillna('')
|
||||
|
||||
# If we have less than 2 rows, we cannot import
|
||||
rowNumber = len(csvDF.index)
|
||||
if rowNumber < 2:
|
||||
raise ValueError("Invalid CSV file data - Not enough rows.")
|
||||
|
||||
if len(csvDF.columns) < 1:
|
||||
raise ValueError("Invalid CSV file data - Not enough columns.")
|
||||
|
||||
importEntityCSVDialog = ImportEntityFromCSVFile(self, csvDF)
|
||||
if importEntityCSVDialog.exec_():
|
||||
attributeRows = [comboBox.currentText()
|
||||
@@ -588,22 +589,26 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
self.parent().LENTDB.addEntity(existingEntity, updateTimeline=False)
|
||||
|
||||
elif importDialog.CSVFileChoiceLinks.isChecked():
|
||||
csvDF = pd.read_csv(fileDirectory)
|
||||
try:
|
||||
csvDF = pd.read_excel(fileDirectory)
|
||||
except ValueError:
|
||||
csvDF = pd.read_csv(fileDirectory)
|
||||
|
||||
# If we have no rows or columns, we cannot import anything.
|
||||
# This is essentially a sanity check.
|
||||
if len(csvDF.index) < 1:
|
||||
raise ValueError("Invalid import file data - Not enough rows.")
|
||||
if len(csvDF.columns) < 1:
|
||||
raise ValueError("Invalid import file data - Not enough columns.")
|
||||
|
||||
# Remove duplicate column names
|
||||
# When importing, all columns should be given unique values,
|
||||
# so this should be just another sanity check.
|
||||
csvDF = csvDF.loc[:, ~csvDF.columns.duplicated()]
|
||||
|
||||
# Fill NaN values with an empty string
|
||||
csvDF.fillna('')
|
||||
|
||||
# If we have less than 2 rows, we cannot import
|
||||
rowNumber = len(csvDF.index)
|
||||
if rowNumber < 2:
|
||||
raise ValueError("Invalid CSV file data - Not enough rows.")
|
||||
|
||||
if len(csvDF.columns) < 1:
|
||||
raise ValueError("Invalid CSV file data - Not enough columns.")
|
||||
|
||||
importLinksCSVDialog = ImportLinksFromCSVFile(self, csvDF)
|
||||
if importLinksCSVDialog.exec_():
|
||||
unmapped = []
|
||||
@@ -1167,12 +1172,33 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
steps = 3
|
||||
progress = QtWidgets.QProgressDialog('Importing tabs, please wait...',
|
||||
'Abort Import', 0, steps, self)
|
||||
progress.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progress.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
progress.setMinimumDuration(0)
|
||||
importTabsThread.progressSignal.connect(progress.setValue)
|
||||
progress.canceled.connect(lambda: importTabsThread.cancelOperation())
|
||||
importTabsThread.start()
|
||||
|
||||
def importFromTORBrowser(self) -> None:
|
||||
"""
|
||||
Import TOR session tabs to canvas.
|
||||
|
||||
:return:
|
||||
"""
|
||||
|
||||
importDialog = TORBrowserImportDialog(self)
|
||||
|
||||
if importDialog.exec_():
|
||||
importTorTabsThread = ImportTorBrowserTabsThread(importDialog, self.parent(), self)
|
||||
|
||||
steps = 4
|
||||
progress = QtWidgets.QProgressDialog('Importing tabs, please wait...',
|
||||
'Abort Import', 0, steps, self)
|
||||
progress.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
progress.setMinimumDuration(0)
|
||||
importTorTabsThread.progressSignal.connect(progress.setValue)
|
||||
progress.canceled.connect(lambda: importTorTabsThread.cancelOperation())
|
||||
importTorTabsThread.start()
|
||||
|
||||
def firefoxCookiesHelper(self, cookiesDatabasePath: Path) -> list:
|
||||
"""
|
||||
Used by threads to get firefox's cookies.
|
||||
@@ -1225,107 +1251,25 @@ class MenuBar(QtWidgets.QMenuBar):
|
||||
cookieHash.update(chunk)
|
||||
return cookieHash.digest()
|
||||
|
||||
def importBrowserTabsFindings(self, resolution_result: list, importToCanvas: Union[bool, None] = None,
|
||||
canvasToImportTo: Union[str, None] = None) -> None:
|
||||
# See the function 'facilitateResolution' in CentralPane for guidance.
|
||||
|
||||
# Get all the entities, then split it into several lists, to make searching & iterating through them faster.
|
||||
allEntities = [(entity['uid'], (entity[list(entity)[1]], entity['Entity Type']))
|
||||
for entity in self.parent().LENTDB.getAllEntities()]
|
||||
if allEntities:
|
||||
allEntityUIDs, allEntityPrimaryFieldsAndTypes = map(list, zip(*allEntities))
|
||||
else:
|
||||
allEntityUIDs = []
|
||||
allEntityPrimaryFieldsAndTypes = []
|
||||
allLinks = [linkUID['uid'] for linkUID in self.parent().LENTDB.getAllLinks()]
|
||||
links = []
|
||||
newNodeUIDs = []
|
||||
for resultList in resolution_result:
|
||||
newNodeJSON = resultList[0]
|
||||
newNodeEntityType = newNodeJSON['Entity Type']
|
||||
# Cannot assume proper order of dicts sent over the net.
|
||||
newNodePrimaryFieldKey = self.parent().RESOURCEHANDLER.getPrimaryFieldForEntityType(newNodeEntityType)
|
||||
newNodePrimaryField = newNodeJSON[newNodePrimaryFieldKey]
|
||||
|
||||
try:
|
||||
# Attempt to get the index of an existing entity that shares primary field and type with the new
|
||||
# entity. Those two entities are considered to be referring to the same thing.
|
||||
newNodeExistsIndex = allEntityPrimaryFieldsAndTypes.index((newNodePrimaryField, newNodeEntityType))
|
||||
|
||||
# If entity already exists, update the fields and re-add
|
||||
newNodeExistingUID = allEntityUIDs[newNodeExistsIndex]
|
||||
existingEntityJSON = self.parent().LENTDB.getEntity(newNodeExistingUID)
|
||||
# Remove primary field and entity type, since those are duplicates. Primary field is the first element.
|
||||
del newNodeJSON['Entity Type']
|
||||
del newNodeJSON[newNodePrimaryFieldKey]
|
||||
try:
|
||||
notesField = newNodeJSON.pop('Notes')
|
||||
existingEntityJSON['Notes'] += f'\n{notesField}'
|
||||
except KeyError:
|
||||
# If no new field was actually added to the entity, don't re-add to the database
|
||||
if len(newNodeJSON) == 0:
|
||||
newNodeUIDs.append(newNodeExistingUID)
|
||||
continue
|
||||
# Remove any 'None' values from new nodes - we want to keep all collected info.
|
||||
for potentiallyNoneKey, potentiallyNoneValue in dict(newNodeJSON).items():
|
||||
if potentiallyNoneValue is None or potentiallyNoneValue == 'None':
|
||||
del newNodeJSON[potentiallyNoneKey]
|
||||
# Update old values to new ones, and add new ones where applicable.
|
||||
existingEntityJSON.update(newNodeJSON)
|
||||
self.parent().LENTDB.addEntity(existingEntityJSON, fromServer=True, updateTimeline=False)
|
||||
newNodeUIDs.append(newNodeExistingUID)
|
||||
except ValueError:
|
||||
# If there is no index for which the primary field and entity type of the new node match one of the
|
||||
# existing ones, the node must indeed be new. We add it here.
|
||||
entityJson = self.parent().LENTDB.addEntity(newNodeJSON, fromServer=True, updateTimeline=False)
|
||||
newNodeUIDs.append(entityJson['uid'])
|
||||
# Ensure that different entities involved in the resolution can't independently
|
||||
# create the same new entities.
|
||||
allEntityUIDs.append(entityJson['uid'])
|
||||
allEntityPrimaryFieldsAndTypes.append((newNodePrimaryField, newNodeEntityType))
|
||||
|
||||
for parentsDictHolder, outputEntityUID in zip(resolution_result, newNodeUIDs):
|
||||
if len(parentsDictHolder) > 1:
|
||||
parentsDict = parentsDictHolder[1]
|
||||
for parentID in parentsDict:
|
||||
parentUID = parentID
|
||||
if isinstance(parentUID, int):
|
||||
parentUID = newNodeUIDs[parentUID]
|
||||
# Sanity check: Check that the node that was used for this resolution still exists.
|
||||
if parentUID in allEntityUIDs:
|
||||
resolutionName = parentsDict[parentID]['Resolution']
|
||||
newLinkUID = (parentUID, outputEntityUID)
|
||||
# Avoid creating more links between the same two entities.
|
||||
if newLinkUID in allLinks:
|
||||
linkJson = self.parent().LENTDB.getLinkIfExists(newLinkUID)
|
||||
if resolutionName not in linkJson['Notes']:
|
||||
linkJson['Notes'] += f'\nConnection also produced by Resolution: {resolutionName}'
|
||||
self.parent().LENTDB.addLink(linkJson, fromServer=True)
|
||||
else:
|
||||
self.parent().LENTDB.addLink({'uid': newLinkUID, 'Resolution': resolutionName,
|
||||
'Notes': parentsDict[parentID].get('Notes', '')},
|
||||
fromServer=True)
|
||||
links.append((parentUID, outputEntityUID, resolutionName))
|
||||
allLinks.append(newLinkUID)
|
||||
|
||||
self.parent().syncDatabase()
|
||||
if importToCanvas:
|
||||
def importBrowserTabsFindings(self, resolution_result: list, canvasToImportTo: Union[str, None] = None) -> None:
|
||||
for finding in resolution_result:
|
||||
if len(finding) < 2:
|
||||
# Add dummy parents
|
||||
finding.append({'@^@^@^@': {'Resolution': 'Browser Import', 'Notes': ''}})
|
||||
newNodeUIDs = self.parent().centralWidget().tabbedPane.facilitateResolution('Importing Entities from Browser',
|
||||
resolution_result)
|
||||
if canvasToImportTo:
|
||||
sceneToAddTo = self.parent().centralWidget().tabbedPane.getSceneByName(canvasToImportTo)
|
||||
for newNodeUID in newNodeUIDs:
|
||||
if newNodeUID is not None and newNodeUID not in sceneToAddTo.sceneGraph.nodes:
|
||||
sceneToAddTo.addNodeProgrammatic(newNodeUID)
|
||||
sceneToAddTo.rearrangeGraph()
|
||||
self.parent().centralWidget().tabbedPane.addLinksToTabs(links, "Browser Import")
|
||||
self.parent().LENTDB.resetTimeline()
|
||||
self.parent().saveProject()
|
||||
self.parent().MESSAGEHANDLER.info('Imported tabs from browser successfully.')
|
||||
|
||||
|
||||
class DeleteProjectConfirmationDialog(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, mainWindowObject, currentServerProject: str):
|
||||
super(DeleteProjectConfirmationDialog, self).__init__()
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.setModal(True)
|
||||
self.setLayout(QtWidgets.QVBoxLayout())
|
||||
self.mainWindowObject = mainWindowObject
|
||||
@@ -1334,7 +1278,7 @@ class DeleteProjectConfirmationDialog(QtWidgets.QDialog):
|
||||
resolutionsLabel = QtWidgets.QLabel(f'Delete Project: "{currentServerProject}" ?')
|
||||
resolutionsLabel.setWordWrap(True)
|
||||
|
||||
resolutionsLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
resolutionsLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.layout().addWidget(resolutionsLabel)
|
||||
|
||||
buttonsWidget = QtWidgets.QWidget()
|
||||
@@ -1371,9 +1315,7 @@ class BrowserImportDialog(QtWidgets.QDialog):
|
||||
firefoxGroupLayout = QtWidgets.QVBoxLayout()
|
||||
firefoxGroup.setLayout(firefoxGroupLayout)
|
||||
self.firefoxChoice = QtWidgets.QCheckBox('Get tabs from Firefox')
|
||||
self.firefoxChoice.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
||||
self.firefoxSessionChoice = QtWidgets.QCheckBox('Get entire session instead of latest tabs')
|
||||
self.firefoxSessionChoice.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
||||
firefoxGroupLayout.addWidget(self.firefoxChoice)
|
||||
firefoxGroupLayout.addWidget(self.firefoxSessionChoice)
|
||||
|
||||
@@ -1381,18 +1323,15 @@ class BrowserImportDialog(QtWidgets.QDialog):
|
||||
chromeGroupLayout = QtWidgets.QVBoxLayout()
|
||||
chromeGroup.setLayout(chromeGroupLayout)
|
||||
self.chromeChoice = QtWidgets.QCheckBox('Get tabs from Chrome / Chromium (Experimental)')
|
||||
self.chromeChoice.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
||||
chromeGroupLayout.addWidget(self.chromeChoice)
|
||||
|
||||
dialogLayout.addWidget(firefoxGroup, 1, 0, 1, 2)
|
||||
dialogLayout.addWidget(chromeGroup, 2, 0, 1, 2)
|
||||
|
||||
self.importScreenshotsCheckbox = QtWidgets.QCheckBox('Take screenshots of sites')
|
||||
self.importScreenshotsCheckbox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
||||
dialogLayout.addWidget(self.importScreenshotsCheckbox, 3, 0, 1, 2)
|
||||
|
||||
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
|
||||
self.importToCanvasCheckbox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
||||
self.importToCanvasDropdown = QtWidgets.QComboBox()
|
||||
self.importToCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
|
||||
self.importToCanvasDropdown.setEditable(False)
|
||||
@@ -1419,6 +1358,39 @@ class BrowserImportDialog(QtWidgets.QDialog):
|
||||
dialogLayout.addWidget(acceptButton, 5, 1, 1, 1)
|
||||
|
||||
|
||||
class TORBrowserImportDialog(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
super(TORBrowserImportDialog, self).__init__(parent=parent)
|
||||
self.setWindowTitle('Import TOR Browser Tabs')
|
||||
self.setModal(True)
|
||||
|
||||
dialogLayout = QtWidgets.QGridLayout()
|
||||
self.setLayout(dialogLayout)
|
||||
self.entireSessionChoice = QtWidgets.QCheckBox('Get entire session instead of latest tabs')
|
||||
dialogLayout.addWidget(self.entireSessionChoice, 0, 0, 1, 2)
|
||||
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
|
||||
self.importToCanvasDropdown = QtWidgets.QComboBox()
|
||||
self.importToCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
|
||||
self.importToCanvasDropdown.setEditable(False)
|
||||
self.importToCanvasDropdown.setDisabled(True)
|
||||
self.importToCanvasCheckbox.toggled.connect(lambda: self.importToCanvasDropdown.setDisabled(
|
||||
self.importToCanvasDropdown.isEnabled()))
|
||||
dialogLayout.addWidget(self.importToCanvasCheckbox, 1, 0, 1, 2)
|
||||
dialogLayout.addWidget(self.importToCanvasDropdown, 2, 0, 1, 2)
|
||||
|
||||
acceptButton = QtWidgets.QPushButton('Accept')
|
||||
acceptButton.setAutoDefault(True)
|
||||
acceptButton.setDefault(True)
|
||||
cancelButton = QtWidgets.QPushButton('Cancel')
|
||||
acceptButton.clicked.connect(self.accept)
|
||||
cancelButton.clicked.connect(self.reject)
|
||||
acceptButton.setFocus()
|
||||
|
||||
dialogLayout.addWidget(cancelButton, 4, 0, 1, 1)
|
||||
dialogLayout.addWidget(acceptButton, 4, 1, 1, 1)
|
||||
|
||||
|
||||
class ServerConnectWizard(QtWidgets.QDialog):
|
||||
"""
|
||||
Dialog Window that lets the user input the details of the server that
|
||||
@@ -1441,7 +1413,7 @@ class ServerConnectWizard(QtWidgets.QDialog):
|
||||
|
||||
serverPasswordLabel = QtWidgets.QLabel("Server Password:")
|
||||
self.serverPasswordTextbox = QtWidgets.QLineEdit()
|
||||
self.serverPasswordTextbox.setEchoMode(QtWidgets.QLineEdit.Password)
|
||||
self.serverPasswordTextbox.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
|
||||
self.layout().addRow(serverPasswordLabel, self.serverPasswordTextbox)
|
||||
|
||||
self.confirmConnect = False
|
||||
@@ -1480,7 +1452,7 @@ class ServerCreateOrOpenProject(QtWidgets.QDialog):
|
||||
self.openProjectDropdown.addItems(serverProjects)
|
||||
self.openProjectPassword = QtWidgets.QLineEdit('')
|
||||
self.openProjectPassword.setToolTip('Enter the password of the selected server project.')
|
||||
self.openProjectPassword.setEchoMode(QtWidgets.QLineEdit.Password)
|
||||
self.openProjectPassword.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
|
||||
openProjectLayout.addRow('Open Project:', self.openProjectDropdown)
|
||||
openProjectLayout.addRow('Password:', self.openProjectPassword)
|
||||
openProjectButton = QtWidgets.QPushButton('Open Project')
|
||||
@@ -1494,7 +1466,7 @@ class ServerCreateOrOpenProject(QtWidgets.QDialog):
|
||||
self.createProjectNameTextbox = QtWidgets.QLineEdit('')
|
||||
self.createProjectNameTextbox.setToolTip('Specify the name of the server project. Must be unique.')
|
||||
self.createProjectPasswordTextbox = QtWidgets.QLineEdit('')
|
||||
self.createProjectPasswordTextbox.setEchoMode(QtWidgets.QLineEdit.Password)
|
||||
self.createProjectPasswordTextbox.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
|
||||
self.createProjectPasswordTextbox.setToolTip('Specify a password to be entered'
|
||||
' in order to access this project.')
|
||||
|
||||
@@ -1531,14 +1503,13 @@ class ViewAndStopResolutionsDialog(QtWidgets.QDialog):
|
||||
|
||||
def __init__(self, mainWindowObject):
|
||||
super(ViewAndStopResolutionsDialog, self).__init__()
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.setModal(True)
|
||||
self.setLayout(QtWidgets.QVBoxLayout())
|
||||
self.mainWindowObject = mainWindowObject
|
||||
|
||||
resolutionsLabel = QtWidgets.QLabel('Running Resolutions:')
|
||||
|
||||
resolutionsLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
resolutionsLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.layout().addWidget(resolutionsLabel)
|
||||
|
||||
scrollArea = QtWidgets.QScrollArea()
|
||||
@@ -1566,7 +1537,6 @@ class ViewAndStopResolutionsDialogOption(QtWidgets.QPushButton):
|
||||
|
||||
def __init__(self, resolutionThread, fromServer, mainWindowObject):
|
||||
super(ViewAndStopResolutionsDialogOption, self).__init__()
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.mainWindowObject = mainWindowObject
|
||||
self.resolutionThread = resolutionThread
|
||||
self.fromServer = fromServer
|
||||
@@ -1588,14 +1558,13 @@ class CollectorsDialog(QtWidgets.QDialog):
|
||||
def __init__(self, mainWindow, collectorsDict: dict = None, runningCollectors: dict = None):
|
||||
super(CollectorsDialog, self).__init__()
|
||||
self.mainWindow = mainWindow
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.baseLayout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(self.baseLayout)
|
||||
self.setModal(True)
|
||||
self.runningCollectorTreeItems = {}
|
||||
|
||||
collectorsLabel = QtWidgets.QLabel("Collectors")
|
||||
collectorsLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
collectorsLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.baseLayout.addWidget(collectorsLabel)
|
||||
|
||||
if collectorsDict is None:
|
||||
@@ -1625,9 +1594,10 @@ class CollectorsDialog(QtWidgets.QDialog):
|
||||
newCollectorInstanceTree = QtWidgets.QTreeWidget()
|
||||
newCollectorInstanceTree.setColumnCount(2)
|
||||
newCollectorInstanceTree.setHeaderLabels(['UID', 'Stop Button'])
|
||||
newCollectorInstanceTree.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||
newCollectorInstanceTree.setSelectionBehavior(
|
||||
QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
newCollectorInstanceTree.header().setStretchLastSection(False)
|
||||
newCollectorInstanceTree.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
||||
newCollectorInstanceTree.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch)
|
||||
newCollectorWidgetLayout.addWidget(newCollectorInstanceTree, 1, 0, 2, 2)
|
||||
|
||||
if runningCollectors is not None:
|
||||
@@ -1686,7 +1656,6 @@ class CollectorStartDialog(QtWidgets.QDialog):
|
||||
def __init__(self, entityDB, collectorDict: dict):
|
||||
super(CollectorStartDialog, self).__init__()
|
||||
self.entityDB = entityDB
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.setModal(True)
|
||||
self.setWindowTitle('Collector Wizard')
|
||||
self.parametersList = []
|
||||
@@ -1710,16 +1679,16 @@ class CollectorStartDialog(QtWidgets.QDialog):
|
||||
entitySelectTabLabel.setWordWrap(True)
|
||||
entitySelectTabLabel.setMaximumWidth(600)
|
||||
|
||||
entitySelectTabLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
entitySelectTabLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
entitySelectTab.layout().addWidget(entitySelectTabLabel)
|
||||
|
||||
self.entitySelector = QtWidgets.QTreeWidget()
|
||||
self.entitySelector.setHeaderLabels(['Primary Field', 'Entity Type', 'Icon'])
|
||||
self.entitySelector.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||
self.entitySelector.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.entitySelector.setSortingEnabled(True)
|
||||
# Stretch the first column, since it contains the primary field.
|
||||
self.entitySelector.header().setStretchLastSection(False)
|
||||
self.entitySelector.header().setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
||||
self.entitySelector.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch)
|
||||
relevantEntityFields = [(entity['uid'], entity[list(entity)[1]], entity['Entity Type'], entity['Icon'])
|
||||
for entity in entityDB.getAllEntities()
|
||||
if entity['Entity Type'] in originTypes or '*' in originTypes]
|
||||
@@ -1733,7 +1702,7 @@ class CollectorStartDialog(QtWidgets.QDialog):
|
||||
# Hidden, so we can pull the UID later.
|
||||
newTreeWidgetItem.setText(3, eligibleEntity[0])
|
||||
|
||||
self.entitySelector.setSelectionMode(self.entitySelector.MultiSelection)
|
||||
self.entitySelector.setSelectionMode(self.entitySelector.SelectionMode.MultiSelection)
|
||||
entitySelectTab.layout().addWidget(self.entitySelector)
|
||||
|
||||
self.childWidget.addTab(entitySelectTab, 'Entities')
|
||||
@@ -1748,7 +1717,7 @@ class CollectorStartDialog(QtWidgets.QDialog):
|
||||
propertyLabel.setWordWrap(True)
|
||||
propertyLabel.setMaximumWidth(600)
|
||||
|
||||
propertyLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
propertyLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
propertyKeyLayout.addWidget(propertyLabel)
|
||||
|
||||
propertyType = parameters[key].get('type')
|
||||
@@ -1769,7 +1738,6 @@ class CollectorStartDialog(QtWidgets.QDialog):
|
||||
|
||||
if propertyInputField is not None:
|
||||
propertyKeyLayout.addWidget(propertyInputField)
|
||||
propertyInputField.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
||||
|
||||
propertyKeyLayout.setStretch(1, 1)
|
||||
|
||||
@@ -1777,18 +1745,14 @@ class CollectorStartDialog(QtWidgets.QDialog):
|
||||
self.parametersList.append((key, propertyInputField))
|
||||
|
||||
nextButton = QtWidgets.QPushButton('Next')
|
||||
nextButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
||||
nextButton.clicked.connect(self.nextTab)
|
||||
previousButton = QtWidgets.QPushButton('Previous')
|
||||
previousButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
||||
previousButton.clicked.connect(self.previousTab)
|
||||
acceptButton = QtWidgets.QPushButton('Accept')
|
||||
acceptButton.setAutoDefault(True)
|
||||
acceptButton.setDefault(True)
|
||||
acceptButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
||||
acceptButton.clicked.connect(self.accept)
|
||||
cancelButton = QtWidgets.QPushButton('Cancel')
|
||||
cancelButton.setStyleSheet(Stylesheets.BUTTON_STYLESHEET_2)
|
||||
cancelButton.clicked.connect(self.reject)
|
||||
|
||||
dialogLayout.addWidget(previousButton, 4, 0, 1, 1)
|
||||
@@ -1829,7 +1793,6 @@ class ImportLinksFromCSVFile(QtWidgets.QDialog):
|
||||
def __init__(self, parent, csvTableContents: pd.DataFrame):
|
||||
super(ImportLinksFromCSVFile, self).__init__(parent=parent)
|
||||
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.setModal(True)
|
||||
importLayout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(importLayout)
|
||||
@@ -1838,7 +1801,7 @@ class ImportLinksFromCSVFile(QtWidgets.QDialog):
|
||||
columnNumber = len(csvTableContents.columns)
|
||||
|
||||
titleLabel = QtWidgets.QLabel("Import Links from CSV")
|
||||
titleLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
titleLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
descLabel = QtWidgets.QLabel("Select the Entity Type of the Parent entity (Entity One) and the Entity Type of "
|
||||
"the Child entity (Entity Two). Then, map these entities to columns in the CSV "
|
||||
@@ -1894,7 +1857,7 @@ class ImportLinksFromCSVFile(QtWidgets.QDialog):
|
||||
rowValues = list(row)
|
||||
for column in range(columnNumber):
|
||||
columnItem = QtWidgets.QTableWidgetItem(str(rowValues[column + 1]))
|
||||
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemIsEditable)
|
||||
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable)
|
||||
csvTable.setItem(rowValues[0], column, columnItem)
|
||||
|
||||
randomizationLabel = QtWidgets.QLabel("If the resolution identifiers (i.e. 'Resolution ID') are not guaranteed "
|
||||
@@ -1903,7 +1866,7 @@ class ImportLinksFromCSVFile(QtWidgets.QDialog):
|
||||
"have random tokens as the Resolution IDs.\nPlease select what you would "
|
||||
"like to do:")
|
||||
randomizationLabel.setWordWrap(True)
|
||||
randomizationLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
randomizationLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.randAsIs = QtWidgets.QRadioButton("Use 'Resolution ID' as-is.")
|
||||
self.randMerge = QtWidgets.QRadioButton("Append a random token to the values mapped to the 'Resolution ID' "
|
||||
"field.")
|
||||
@@ -1969,7 +1932,6 @@ class ImportLinkEntitiesFromCSVFile(QtWidgets.QDialog):
|
||||
def __init__(self, parent, csvTableContents: pd.DataFrame):
|
||||
super(ImportLinkEntitiesFromCSVFile, self).__init__(parent=parent)
|
||||
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.setModal(True)
|
||||
importLayout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(importLayout)
|
||||
@@ -1978,7 +1940,7 @@ class ImportLinkEntitiesFromCSVFile(QtWidgets.QDialog):
|
||||
columnNumber = len(csvTableContents.columns)
|
||||
|
||||
titleLabel = QtWidgets.QLabel("Create Entities from Link Fields")
|
||||
titleLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
titleLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
descLabel = QtWidgets.QLabel("Select the entity type to import the remaining fields as. "
|
||||
"Then, configure the mapping between the CSV fields and entity attributes. To do "
|
||||
@@ -2028,7 +1990,7 @@ class ImportLinkEntitiesFromCSVFile(QtWidgets.QDialog):
|
||||
rowValues = list(row)
|
||||
for column in range(columnNumber):
|
||||
columnItem = QtWidgets.QTableWidgetItem(str(rowValues[column + 1]))
|
||||
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemIsEditable)
|
||||
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable)
|
||||
csvTable.setItem(rowValues[0], column, columnItem)
|
||||
|
||||
buttonsWidget = QtWidgets.QWidget()
|
||||
@@ -2082,7 +2044,6 @@ class ImportEntityFromCSVFile(QtWidgets.QDialog):
|
||||
def __init__(self, parent, csvTableContents: pd.DataFrame):
|
||||
super(ImportEntityFromCSVFile, self).__init__(parent=parent)
|
||||
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
self.setModal(True)
|
||||
importLayout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(importLayout)
|
||||
@@ -2091,7 +2052,7 @@ class ImportEntityFromCSVFile(QtWidgets.QDialog):
|
||||
columnNumber = len(csvTableContents.columns)
|
||||
|
||||
titleLabel = QtWidgets.QLabel("Import Entities from CSV")
|
||||
titleLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
titleLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
descLabel = QtWidgets.QLabel("Select the entity type to import the entities from the CSV file as. "
|
||||
"Then, configure the mapping between the CSV fields and entity attributes. To do "
|
||||
@@ -2131,14 +2092,13 @@ class ImportEntityFromCSVFile(QtWidgets.QDialog):
|
||||
rowValues = list(row)
|
||||
for column in range(columnNumber):
|
||||
columnItem = QtWidgets.QTableWidgetItem(str(rowValues[column + 1]))
|
||||
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemIsEditable)
|
||||
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable)
|
||||
csvTable.setItem(rowValues[0], column, columnItem)
|
||||
|
||||
importToCanvasChoiceWidget = QtWidgets.QWidget()
|
||||
importToCanvasChoiceLayout = QtWidgets.QHBoxLayout()
|
||||
importToCanvasChoiceWidget.setLayout(importToCanvasChoiceLayout)
|
||||
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
|
||||
self.importToCanvasCheckbox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
||||
self.importToCanvasDropdown = QtWidgets.QComboBox()
|
||||
self.importToCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
|
||||
self.importToCanvasDropdown.setEditable(False)
|
||||
@@ -2201,10 +2161,11 @@ class ImportEntityFromCSVFile(QtWidgets.QDialog):
|
||||
class ImportFromFileDialog(QtWidgets.QDialog):
|
||||
|
||||
def popupFileDialog(self):
|
||||
self.fileDirectory = QtWidgets.QFileDialog().getOpenFileName(parent=self, caption='Select File to Import From',
|
||||
dir=str(Path.home()),
|
||||
options=QtWidgets.QFileDialog.DontUseNativeDialog,
|
||||
filter="CSV or txt (*.csv *.txt)")[0]
|
||||
self.fileDirectory = QtWidgets.QFileDialog().getOpenFileName(
|
||||
parent=self, caption='Select File to Import From',
|
||||
dir=str(Path.home()),
|
||||
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog,
|
||||
filter="Import File (*.csv *.txt *.xls *.xlsx *.ods)")[0]
|
||||
if self.fileDirectory != '':
|
||||
self.fileDirectoryLine.setText(self.fileDirectory)
|
||||
|
||||
@@ -2217,7 +2178,7 @@ class ImportFromFileDialog(QtWidgets.QDialog):
|
||||
dialogLayout = QtWidgets.QGridLayout()
|
||||
self.setLayout(dialogLayout)
|
||||
descriptionLabel = QtWidgets.QLabel('Select the file to import Entities or Links from:')
|
||||
descriptionLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
descriptionLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
descriptionLabel.setWordWrap(True)
|
||||
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
|
||||
|
||||
@@ -2226,16 +2187,13 @@ class ImportFromFileDialog(QtWidgets.QDialog):
|
||||
self.fileDirectoryLine.setReadOnly(True)
|
||||
|
||||
fileChoiceLabel = QtWidgets.QLabel('Specify the type of the chosen file and what to import:')
|
||||
fileChoiceLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
fileChoiceLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
fileChoiceLabel.setWordWrap(True)
|
||||
|
||||
self.textFileChoice = QtWidgets.QRadioButton('Text file - Entities Import')
|
||||
self.textFileChoice.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
|
||||
self.textFileChoice.setChecked(True)
|
||||
self.CSVFileChoice = QtWidgets.QRadioButton('CSV - Entities Import')
|
||||
self.CSVFileChoice.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
|
||||
self.CSVFileChoiceLinks = QtWidgets.QRadioButton('CSV - Links Import')
|
||||
self.CSVFileChoiceLinks.setStyleSheet(Stylesheets.RADIO_BUTTON_STYLESHEET)
|
||||
self.CSVFileChoice = QtWidgets.QRadioButton('Spreadsheet / CSV - Entities Import')
|
||||
self.CSVFileChoiceLinks = QtWidgets.QRadioButton('Spreadsheet / CSV - Links Import')
|
||||
|
||||
dialogLayout.addWidget(self.fileDirectoryLine, 1, 0, 1, 2)
|
||||
dialogLayout.addWidget(self.fileDirectoryButton, 2, 0, 1, 2)
|
||||
@@ -2272,7 +2230,7 @@ class ImportFromTextFileDialog(QtWidgets.QDialog):
|
||||
dialogLayout = QtWidgets.QGridLayout()
|
||||
self.setLayout(dialogLayout)
|
||||
descriptionLabel = QtWidgets.QLabel('Importing entities from text file, one entity per line.')
|
||||
descriptionLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||
descriptionLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
descriptionLabel.setWordWrap(True)
|
||||
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
|
||||
|
||||
@@ -2299,7 +2257,7 @@ class ImportFromTextFileDialog(QtWidgets.QDialog):
|
||||
textTable.setFixedWidth(450)
|
||||
for lineIndex, lineValue in enumerate(fileContents):
|
||||
columnItem = QtWidgets.QTableWidgetItem(lineValue)
|
||||
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemIsEditable)
|
||||
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable)
|
||||
textTable.setItem(lineIndex, 0, columnItem)
|
||||
textTable.setColumnWidth(0, 450)
|
||||
textTable.setHorizontalHeaderLabels(['File Entities Preview'])
|
||||
@@ -2307,7 +2265,6 @@ class ImportFromTextFileDialog(QtWidgets.QDialog):
|
||||
dialogLayout.addWidget(textTable, 3, 0, 1, 2)
|
||||
|
||||
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
|
||||
self.importToCanvasCheckbox.setStyleSheet(Stylesheets.CHECK_BOX_STYLESHEET)
|
||||
self.importToCanvasDropdown = QtWidgets.QComboBox()
|
||||
self.importToCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
|
||||
self.importToCanvasDropdown.setEditable(False)
|
||||
@@ -2347,7 +2304,6 @@ class CanvasPictureDialog(QtWidgets.QDialog):
|
||||
self.fileDirectory = ""
|
||||
self.setWindowTitle('Save Canvas Picture')
|
||||
self.setModal(True)
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
|
||||
dialogLayout = QtWidgets.QGridLayout()
|
||||
self.setLayout(dialogLayout)
|
||||
@@ -2411,12 +2367,11 @@ class CanvasPictureDialog(QtWidgets.QDialog):
|
||||
|
||||
def popupFileDialog(self):
|
||||
saveAsDialog = QtWidgets.QFileDialog()
|
||||
saveAsDialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True)
|
||||
saveAsDialog.setViewMode(QtWidgets.QFileDialog.List)
|
||||
saveAsDialog.setOption(QtWidgets.QFileDialog.Option.DontUseNativeDialog, True)
|
||||
saveAsDialog.setViewMode(QtWidgets.QFileDialog.ViewMode.List)
|
||||
saveAsDialog.setNameFilter("Image (*.png)")
|
||||
saveAsDialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
|
||||
saveAsDialog.setAcceptMode(QtWidgets.QFileDialog.AcceptMode.AcceptSave)
|
||||
saveAsDialog.setDirectory(str(Path.home()))
|
||||
saveAsDialog.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
saveAsDialog.exec()
|
||||
self.fileDirectory = saveAsDialog.selectedFiles()[0]
|
||||
if self.fileDirectory != '':
|
||||
@@ -2436,7 +2391,6 @@ class SearchEngineDialog(QtWidgets.QDialog):
|
||||
super(SearchEngineDialog, self).__init__(parent=parent)
|
||||
self.setWindowTitle('Search Engine Lookup')
|
||||
self.setModal(True)
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
@@ -2473,7 +2427,6 @@ class SearchImageEngineDialog(QtWidgets.QDialog):
|
||||
super(SearchImageEngineDialog, self).__init__(parent=parent)
|
||||
self.setWindowTitle('Image Search Engine Lookup')
|
||||
self.setModal(True)
|
||||
self.setStyleSheet(Stylesheets.MAIN_WINDOW_STYLESHEET)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
@@ -2530,6 +2483,8 @@ class ScreenshotWebsiteThread(QtCore.QThread):
|
||||
user_agent=user_agents['Firefox']['Linux'][0]
|
||||
)
|
||||
urlPath = Path.home() / '.mozilla' / 'firefox'
|
||||
if not (urlPath / 'profiles.ini').exists():
|
||||
urlPath = Path.home() / 'snap' / 'firefox' / 'common' / '.mozilla' / 'firefox'
|
||||
else: # We already checked before that the platform is either 'Linux' or 'Windows'.
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
@@ -2621,6 +2576,8 @@ class SaveWebsiteThread(QtCore.QThread):
|
||||
user_agent=user_agents['Firefox']['Linux'][0]
|
||||
)
|
||||
urlPath = Path.home() / '.mozilla' / 'firefox'
|
||||
if not (urlPath / 'profiles.ini').exists():
|
||||
urlPath = Path.home() / 'snap' / 'firefox' / 'common' / '.mozilla' / 'firefox'
|
||||
else: # We already checked before that the platform is either 'Linux' or 'Windows'.
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
@@ -2697,6 +2654,8 @@ class ImportBrowserTabsThread(QtCore.QThread):
|
||||
user_agent=user_agents['Firefox']['Linux'][0]
|
||||
)
|
||||
urlPath = Path.home() / '.mozilla' / 'firefox'
|
||||
if not (urlPath / 'profiles.ini').exists():
|
||||
urlPath = Path.home() / 'snap' / 'firefox' / 'common' / '.mozilla' / 'firefox'
|
||||
else: # We already checked before that the platform is either 'Linux' or 'Windows'.
|
||||
context = browser.new_context(
|
||||
viewport={'width': 1920, 'height': 1080},
|
||||
@@ -2722,12 +2681,13 @@ class ImportBrowserTabsThread(QtCore.QThread):
|
||||
first = True
|
||||
for browserEntry in browserTab['entries']:
|
||||
url = browserEntry['url']
|
||||
title = browserEntry.get('title', '')
|
||||
if not url.startswith('about:'):
|
||||
if first:
|
||||
tabsToOpen.append((url, browserEntry['title'], True))
|
||||
tabsToOpen.append((url, title, True))
|
||||
first = False
|
||||
else:
|
||||
tabsToOpen.append((url, browserEntry['title'], False))
|
||||
tabsToOpen.append((url, title, False))
|
||||
else:
|
||||
browserEntry = browserTab['entries'][browserTab['index'] - 1]
|
||||
url = browserEntry['url']
|
||||
@@ -2823,7 +2783,9 @@ class ImportBrowserTabsThread(QtCore.QThread):
|
||||
[screenshotEntity,
|
||||
{len(returnResults) + 1: {'Resolution': 'Screenshot of Tab',
|
||||
'Notes': ''}}])
|
||||
|
||||
else:
|
||||
newEntity = [{'Phrase': actualURL,
|
||||
'Entity Type': 'Phrase'}]
|
||||
if len(tabToOpen) == 3:
|
||||
if historyMark != -1 and not tabToOpen[2]:
|
||||
newEntity.append({historyMark: {'Resolution': 'Next Page'}})
|
||||
@@ -2999,5 +2961,151 @@ class ImportBrowserTabsThread(QtCore.QThread):
|
||||
progressValue = 3
|
||||
self.progressSignal.emit(progressValue)
|
||||
self.menuObject.browserTabsImportDoneSignalListener.emit(
|
||||
returnResults, self.importDialog.importToCanvasCheckbox.isChecked(),
|
||||
self.importDialog.importToCanvasDropdown.currentText())
|
||||
returnResults,
|
||||
self.importDialog.importToCanvasDropdown.currentText() if
|
||||
self.importDialog.importToCanvasCheckbox.isChecked() else '')
|
||||
|
||||
|
||||
class ImportTorBrowserTabsThread(QtCore.QThread):
|
||||
progressSignal = QtCore.Signal(int)
|
||||
cancelled = False
|
||||
|
||||
def __init__(self, importDialog: TORBrowserImportDialog, mainWindowObject, menuObject):
|
||||
super(ImportTorBrowserTabsThread, self).__init__(parent=mainWindowObject)
|
||||
self.importDialog = importDialog
|
||||
self.mainWindow = mainWindowObject
|
||||
self.menuObject = menuObject
|
||||
|
||||
def cancelOperation(self):
|
||||
self.cancelled = True
|
||||
|
||||
def run(self) -> None:
|
||||
|
||||
progressValue = 1
|
||||
self.progressSignal.emit(progressValue)
|
||||
|
||||
returnResults = []
|
||||
|
||||
torBrowserProfilePath = Path(self.mainWindow.SETTINGS.value("Program/TOR Profile Location", "/"))
|
||||
tabsFilePath = torBrowserProfilePath / 'sessionstore-backups' / 'recovery.jsonlz4'
|
||||
if not tabsFilePath.exists():
|
||||
progressValue = 4
|
||||
self.progressSignal.emit(progressValue)
|
||||
self.menuObject.browserTabsImportDoneSignalListener.emit(
|
||||
returnResults,
|
||||
'')
|
||||
return
|
||||
if self.importDialog.entireSessionChoice.isChecked():
|
||||
tabsToOpen = []
|
||||
else:
|
||||
tabsToOpen = set()
|
||||
|
||||
tabsBytes = tabsFilePath.read_bytes()
|
||||
if tabsBytes[:8] == b'mozLz40\0':
|
||||
tabsBytes = lz4.block.decompress(tabsBytes[8:])
|
||||
tabsJson = json.loads(tabsBytes)
|
||||
for browserWindow in tabsJson['windows']:
|
||||
for browserTab in browserWindow['tabs']:
|
||||
if self.importDialog.entireSessionChoice.isChecked():
|
||||
first = True
|
||||
for browserEntry in browserTab['entries']:
|
||||
url = browserEntry['url']
|
||||
title = browserEntry.get('title', '')
|
||||
if not url.startswith('about:'):
|
||||
if first:
|
||||
tabsToOpen.append((url, title, True))
|
||||
first = False
|
||||
else:
|
||||
tabsToOpen.append((url, title, False))
|
||||
else:
|
||||
browserEntry = browserTab['entries'][browserTab['index'] - 1]
|
||||
url = browserEntry['url']
|
||||
if not url.startswith('about:'):
|
||||
tabsToOpen.add((url, browserEntry['title']))
|
||||
|
||||
cookiesDatabasePath = torBrowserProfilePath / 'cookies.sqlite'
|
||||
browserCookies = self.menuObject.firefoxCookiesHelper(cookiesDatabasePath)
|
||||
|
||||
historyMark = -1
|
||||
for tabToOpen in tabsToOpen:
|
||||
if self.cancelled:
|
||||
break
|
||||
urlTitle = tabToOpen[1]
|
||||
actualURL = tabToOpen[0]
|
||||
decodedPath = parse.unquote(actualURL)
|
||||
parsedURL = parse.urlparse(decodedPath)
|
||||
urlPath = parsedURL.path
|
||||
|
||||
if parsedURL.scheme == 'file':
|
||||
try:
|
||||
mime = magic.Magic(mime=True)
|
||||
pathType = mime.from_file(urlPath)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
|
||||
if 'application' in pathType:
|
||||
newEntity = [{'Document Name': urlTitle,
|
||||
'File Path': urlPath,
|
||||
'Entity Type': 'Document'}]
|
||||
elif 'image' in pathType:
|
||||
newEntity = [{'Image Name': urlTitle,
|
||||
'File Path': urlPath,
|
||||
'Entity Type': 'Image'}]
|
||||
elif 'video' in pathType:
|
||||
newEntity = [{'Video Name': urlTitle,
|
||||
'File Path': urlPath,
|
||||
'Entity Type': 'Video'}]
|
||||
elif 'archive' in pathType:
|
||||
newEntity = [{'Archive Name': urlTitle,
|
||||
'File Path': urlPath,
|
||||
'Entity Type': 'Archive'}]
|
||||
|
||||
elif parsedURL.scheme.startswith('http'):
|
||||
if '.onion' in actualURL:
|
||||
newEntity = [{'Onion URL': actualURL,
|
||||
'Entity Type': 'Onion Website'}]
|
||||
else:
|
||||
newEntity = [{'URL': actualURL,
|
||||
'Entity Type': 'Website'}]
|
||||
else:
|
||||
newEntity = [{'Phrase': actualURL,
|
||||
'Entity Type': 'Phrase'}]
|
||||
|
||||
if len(tabToOpen) == 3:
|
||||
if historyMark != -1 and not tabToOpen[2]:
|
||||
newEntity.append({historyMark: {'Resolution': 'Next Page'}})
|
||||
historyMark = len(returnResults)
|
||||
returnResults.append(newEntity)
|
||||
|
||||
progressValue = 3
|
||||
self.progressSignal.emit(progressValue)
|
||||
if self.cancelled:
|
||||
self.mainWindow.statusBarSignalListener.emit('Cancelled importing entities from Browser.')
|
||||
else:
|
||||
newTabEntities = len(returnResults)
|
||||
for cookie in browserCookies:
|
||||
cookieParents = []
|
||||
cookieURI = cookie['domain'] + cookie['path']
|
||||
if cookieURI.startswith('.'):
|
||||
cookieURI = cookieURI[1:]
|
||||
for index, tabEntity in enumerate(returnResults[:newTabEntities]):
|
||||
for key, value in tabEntity[0].items():
|
||||
if key != 'Entity Type' and cookieURI in value:
|
||||
cookieParents.append(index)
|
||||
cookieEntity = cookie
|
||||
cookieEntity['Phrase'] = f'Cookie {uuid4()}'
|
||||
cookieEntity['Entity Type'] = 'Phrase'
|
||||
|
||||
cookieParentDict: dict = {}
|
||||
for cookieParent in cookieParents:
|
||||
cookieParentDict[cookieParent] = {'Resolution': 'Site Cookie', 'Notes': ''}
|
||||
|
||||
cookieResult = [cookieEntity, cookieParentDict]
|
||||
returnResults.append(cookieResult)
|
||||
|
||||
progressValue = 4
|
||||
self.progressSignal.emit(progressValue)
|
||||
self.menuObject.browserTabsImportDoneSignalListener.emit(
|
||||
returnResults,
|
||||
self.importDialog.importToCanvasDropdown.currentText() if
|
||||
self.importDialog.importToCanvasCheckbox.isChecked() else '')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -7,8 +7,6 @@ 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
|
||||
|
||||
|
||||
class ResolutionManager:
|
||||
@@ -25,7 +23,6 @@ class ResolutionManager:
|
||||
exceptionsCount = 0
|
||||
for resolution in listdir(directory):
|
||||
resolution = str(resolution)
|
||||
resolutionCategory = "Uncategorized"
|
||||
try:
|
||||
if resolution.endswith('.py'):
|
||||
resolutionName = resolution[:-3]
|
||||
@@ -44,7 +41,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,
|
||||
@@ -153,6 +150,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 +175,9 @@ 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", {})
|
||||
|
||||
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.')
|
||||
|
||||
def save(self):
|
||||
macroFilePath = self.getMacroFilePath()
|
||||
macroFilePathTmp = macroFilePath.with_suffix(f'{macroFilePath.suffix}.tmp')
|
||||
|
||||
with open(macroFilePathTmp, "wb") as macroFile:
|
||||
dump(self.macros, macroFile)
|
||||
move(macroFilePathTmp, macroFilePath)
|
||||
def save(self) -> None:
|
||||
self.mainWindow.SETTINGS.setGlobalValue("Program/Macros", self.macros)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -49,7 +49,7 @@ class AffiliateCodesExtractor:
|
||||
import re
|
||||
|
||||
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,57 +133,55 @@ 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()
|
||||
@@ -197,7 +195,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)
|
||||
|
||||
259
Core/Resolutions/Core/CryptoAddressExtractor.py
Normal file
@@ -0,0 +1,259 @@
|
||||
#!/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
|
||||
import re
|
||||
|
||||
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()
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@@ -38,17 +38,19 @@ class EmailExtractor:
|
||||
from playwright.sync_api import sync_playwright, TimeoutError, Error
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
import contextlib
|
||||
from email_validator import validate_email, caching_resolver, EmailNotValidError
|
||||
|
||||
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()
|
||||
@@ -92,7 +94,7 @@ class EmailExtractor:
|
||||
|
||||
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)
|
||||
@@ -100,24 +102,19 @@ class EmailExtractor:
|
||||
'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.email not in allEmails:
|
||||
allEmails.add(valid.email)
|
||||
returnResults.append([{'Email Address': valid.email,
|
||||
'Entity Type': 'Email Address'},
|
||||
{currentUID: {'Resolution': 'Email Address Found',
|
||||
'Notes': ''}}])
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
@@ -133,7 +130,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
|
||||
|
||||
@@ -29,41 +30,57 @@ class ExtractPDFMeta:
|
||||
if not (filePath.exists() and 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:
|
||||
if metadataKey.startswith('/'):
|
||||
attrValue = metadataKey[1:]
|
||||
else:
|
||||
attrValue = 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]),
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': metadataKey, 'Notes': ''}}])
|
||||
# Clean some misshapen strings
|
||||
value = str(info[metadataKey])
|
||||
if value.startswith('/'):
|
||||
value = value[1:]
|
||||
|
||||
returnResults.append([{'Phrase': 'Number of Pages: ' + str(number_of_pages),
|
||||
'Entity Type': 'Phrase'},
|
||||
returnResults.append([{'Phrase': f'{attrValue}: {value}',
|
||||
'Entity Type': 'Phrase'},
|
||||
{uid: {'Resolution': attrValue, 'Notes': ''}}])
|
||||
|
||||
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,6 +41,7 @@ class FileExtractor:
|
||||
'default': '0'}}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import contextlib
|
||||
import tldextract
|
||||
import requests
|
||||
from hashlib import md5
|
||||
@@ -54,14 +55,15 @@ class FileExtractor:
|
||||
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 +89,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 +105,8 @@ class FileExtractor:
|
||||
fileTypeIdentified = 'Image'
|
||||
elif link.endswith(archiveTypes):
|
||||
fileTypeIdentified = 'Archive'
|
||||
elif link.endswith(spreadsheetTypes):
|
||||
fileTypeIdentified = 'Spreadsheet'
|
||||
|
||||
if fileTypeIdentified:
|
||||
childIndex = len(returnResults)
|
||||
@@ -113,10 +117,10 @@ class FileExtractor:
|
||||
'Notes': ''}}])
|
||||
|
||||
docProperName = link.split('/')[-1]
|
||||
docFileName = hexlify(md5(link.encode()).digest()).decode() + ' | ' + docProperName # nosec
|
||||
docFileName = f'{hexlify(md5(link.encode()).digest()).decode()} | {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 +130,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 +144,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 +156,10 @@ class FileExtractor:
|
||||
{uid: {'Resolution': 'File URL',
|
||||
'Notes': ''}}])
|
||||
docProperName = link.split('/')[-1]
|
||||
docFileName = hexlify(md5(link.encode()).digest()).decode() + ' | ' + docProperName # nosec
|
||||
docFileName = f'{hexlify(md5(link.encode()).digest()).decode()} | {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,9 +174,6 @@ class FileExtractor:
|
||||
'Entity Type': 'Image'},
|
||||
{childIndex: {'Resolution': 'Downloaded File',
|
||||
'Notes': ''}}])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if currentDepth > 0:
|
||||
newDepth = currentDepth - 1
|
||||
for newURL in urlsToExplore:
|
||||
@@ -194,7 +192,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,7 +5,7 @@ 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',
|
||||
@@ -25,10 +25,10 @@ class FileHasher:
|
||||
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
|
||||
|
||||
@@ -83,13 +83,7 @@ class GetExternalURLs:
|
||||
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}
|
||||
else:
|
||||
if domain in link:
|
||||
redirectLinks = redirectRegex.findall(link)
|
||||
if 'redirect' in link and len(redirectLinks) > 0:
|
||||
try:
|
||||
@@ -106,31 +100,35 @@ class GetExternalURLs:
|
||||
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:
|
||||
else:
|
||||
newLink = link.split('#')[0].split('?')[0]
|
||||
try:
|
||||
externalUrls[newLink].add(uid)
|
||||
except KeyError:
|
||||
externalUrls[newLink] = {uid}
|
||||
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]) 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}
|
||||
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 +141,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': ''}}])
|
||||
|
||||
|
||||
@@ -27,9 +27,7 @@ class GetWebsiteText:
|
||||
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')
|
||||
@@ -59,7 +57,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': ''}}])
|
||||
|
||||
@@ -17,7 +17,7 @@ class HostnameToDomain:
|
||||
uid = entity['uid']
|
||||
primary_field = entity[list(entity)[1]].strip()
|
||||
tsd, td, tsu = extract(primary_field)
|
||||
domain = td + '.' + tsu
|
||||
domain = f'{td}.{tsu}'
|
||||
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
|
||||
|
||||
@@ -20,6 +20,8 @@ class JSCodeExtractor:
|
||||
from playwright.sync_api import sync_playwright, Error
|
||||
from base64 import b64decode
|
||||
import re
|
||||
import contextlib
|
||||
|
||||
returnResults = []
|
||||
requestUrlsParsed = set()
|
||||
|
||||
@@ -48,126 +50,127 @@ 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()
|
||||
@@ -194,16 +197,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()
|
||||
|
||||
108
Core/Resolutions/Core/LongANStringExtractor.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/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
|
||||
import re
|
||||
|
||||
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()
|
||||
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
|
||||
38
Core/Resolutions/Core/NPMJSSearch.py
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/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 = set()
|
||||
string_checks.add(''.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
|
||||
@@ -55,12 +55,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:
|
||||
@@ -88,7 +87,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
@@ -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,6 +11,7 @@ class WebsiteFromPhrase:
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import contextlib
|
||||
import re
|
||||
import tldextract
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
|
||||
import contextlib
|
||||
import re
|
||||
from typing import Union
|
||||
from glob import glob
|
||||
|
||||
import networkx as nx
|
||||
import re
|
||||
from datetime import timezone
|
||||
from defusedxml.ElementTree import parse
|
||||
from datetime import datetime
|
||||
@@ -19,8 +20,6 @@ from dateutil import parser
|
||||
from PySide6.QtCore import QByteArray, QSize, QUrl, Qt
|
||||
from PySide6 import QtWidgets, QtGui
|
||||
|
||||
from Core.Interface import Stylesheets
|
||||
|
||||
|
||||
class ResourceHandler:
|
||||
|
||||
@@ -31,62 +30,53 @@ class ResourceHandler:
|
||||
def __init__(self, mainWindow, messageHandler) -> None:
|
||||
self.mainWindow = mainWindow
|
||||
self.messageHandler = messageHandler
|
||||
self.programBaseDirPath = Path(self.mainWindow.SETTINGS.value("Program/BaseDir"))
|
||||
self.entityCategoryList = {}
|
||||
|
||||
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"""^(?:(?: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.loadCoreEntities()
|
||||
|
||||
@@ -208,7 +198,7 @@ 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.programBaseDirPath / "Resources" / "Icons" / icon)}
|
||||
except (KeyError, AttributeError) as err:
|
||||
# Ignore malformed entities
|
||||
self.messageHandler.error(f'Error: {str(err)}', popUp=False)
|
||||
@@ -216,13 +206,13 @@ class ResourceHandler:
|
||||
return True
|
||||
|
||||
def loadCoreEntities(self) -> None:
|
||||
entDir = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Core" / "Entities"
|
||||
entDir = self.programBaseDirPath / "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"
|
||||
entDir = self.programBaseDirPath / "Modules"
|
||||
for module in listdir(entDir):
|
||||
for entFile in listdir(entDir / module):
|
||||
if entFile.endswith('.xml'):
|
||||
@@ -325,7 +315,7 @@ class ResourceHandler:
|
||||
return linkJson
|
||||
|
||||
def getEntityDefaultPicture(self, entityType) -> QByteArray:
|
||||
picture = Path(self.mainWindow.SETTINGS.value("Program/BaseDir")) / "Resources" / "Icons" / "Default.svg"
|
||||
picture = self.programBaseDirPath / "Resources" / "Icons" / "Default.svg"
|
||||
try:
|
||||
for category in self.entityCategoryList:
|
||||
if entityType in self.entityCategoryList[category]:
|
||||
@@ -342,11 +332,11 @@ class ResourceHandler:
|
||||
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 +411,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 +430,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 +456,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 +476,7 @@ class MinSizeStackedLayout(QtWidgets.QStackedLayout):
|
||||
|
||||
https://stackoverflow.com/a/34300567
|
||||
"""
|
||||
|
||||
def sizeHint(self) -> QSize:
|
||||
return self.currentWidget().sizeHint()
|
||||
|
||||
@@ -536,7 +525,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,48 @@ 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.5.0")
|
||||
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"))
|
||||
|
||||
# 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.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 +73,45 @@ 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 = {}
|
||||
for setting in self.globalSettings.allKeys():
|
||||
if setting.startswith(settingsGroup):
|
||||
settingsDict[setting] = self.globalSettings.value(setting)
|
||||
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 +119,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 Exception(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]
|
||||
|
||||
@@ -55,17 +55,26 @@ 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',
|
||||
'vnd.openxmlformats-officedocument.spreadsheetml.sheet'):
|
||||
entityJson = {"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):
|
||||
elif zipfile.is_zipfile(urlPathString):
|
||||
entityJson = {"Archive Name": urlName, "File Path": savePathString, "Entity Type": "Archive"}
|
||||
elif fileTypeSplit1 == "video":
|
||||
entityJson = {"Video Name": urlName, "File Path": savePathString, "Entity Type": "Video"}
|
||||
elif fileTypeSplit1 == "image":
|
||||
entityJson = {"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"}
|
||||
entityJson = {"Document Name": urlName, "File Path": savePathString, "Entity Type": "Document"}
|
||||
return entityJson
|
||||
|
||||
def moveURLToProjectFilesHelperIfNeeded(self, urlPath: Path):
|
||||
|
||||
@@ -783,7 +783,7 @@ 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) + '"',
|
||||
@@ -804,7 +804,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 +815,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 +865,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,8 +888,8 @@ 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()
|
||||
|
||||
@@ -1000,7 +1000,7 @@ 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')
|
||||
@@ -1034,7 +1034,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)
|
||||
|
||||
@@ -1082,7 +1082,7 @@ class LinkScopeInstallLatestPage(QtWidgets.QWizardPage):
|
||||
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 +1097,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)
|
||||
@@ -1135,7 +1135,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 +1183,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 +1202,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)
|
||||
@@ -1235,7 +1235,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)
|
||||
|
||||
|
||||
|
||||
569
LinkScope.py
@@ -48,7 +48,7 @@ class HaveIBeenPwnedBreachDomains:
|
||||
|
||||
breachIconByteArrayFin = QByteArray()
|
||||
breachImageBuffer = QBuffer(breachIconByteArrayFin)
|
||||
breachImageBuffer.open(QIODevice.WriteOnly)
|
||||
breachImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||
breachIconImageRotated.save(breachImageBuffer, "PNG")
|
||||
breachImageBuffer.close()
|
||||
except Exception:
|
||||
|
||||
@@ -51,7 +51,7 @@ class HaveIBeenPwnedBreaches:
|
||||
|
||||
breachIconByteArrayFin = QByteArray()
|
||||
breachImageBuffer = QBuffer(breachIconByteArrayFin)
|
||||
breachImageBuffer.open(QIODevice.WriteOnly)
|
||||
breachImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||
breachIconImageRotated.save(breachImageBuffer, "PNG")
|
||||
breachImageBuffer.close()
|
||||
except Exception:
|
||||
|
||||
@@ -20,7 +20,7 @@ class CompanyInfo:
|
||||
import time
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
|
||||
}
|
||||
|
||||
returnResults = []
|
||||
|
||||
@@ -32,7 +32,7 @@ class FramesLookUp:
|
||||
import time
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -23,7 +23,7 @@ class Get10KForms:
|
||||
import time
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -23,7 +23,7 @@ class Get10QForms:
|
||||
import time
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -27,7 +27,7 @@ class Get13FForms:
|
||||
from ast import literal_eval
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
|
||||
}
|
||||
|
||||
name = ''
|
||||
|
||||
@@ -23,7 +23,7 @@ class Get20FForms:
|
||||
import time
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -27,7 +27,7 @@ class Get3Forms:
|
||||
from ast import literal_eval
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -23,7 +23,7 @@ class Get40FForms:
|
||||
import time
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -27,7 +27,7 @@ class Get4Forms:
|
||||
from ast import literal_eval
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -23,7 +23,7 @@ class Get6KForms:
|
||||
import time
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -23,7 +23,7 @@ class Get8KForms:
|
||||
import time
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -27,7 +27,7 @@ class GetDForms:
|
||||
from ast import literal_eval
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -25,7 +25,7 @@ class GetN8FForms:
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
headers = {
|
||||
'User-Agent': 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0',
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -49,7 +49,7 @@ class InterpolRedNotices:
|
||||
thumbnailIconImageScaled = thumbnailIconImageOriginal.scaled(QSize(40, 40))
|
||||
thumbnailByteArrayFin = QByteArray()
|
||||
thumbnailImageBuffer = QBuffer(thumbnailByteArrayFin)
|
||||
thumbnailImageBuffer.open(QIODevice.WriteOnly)
|
||||
thumbnailImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||
thumbnailIconImageScaled.save(thumbnailImageBuffer, "PNG")
|
||||
thumbnailImageBuffer.close()
|
||||
except Exception:
|
||||
|
||||
@@ -44,7 +44,7 @@ class InterpolYellowNotices:
|
||||
thumbnailIconImageScaled = thumbnailIconImageOriginal.scaled(QSize(40, 40))
|
||||
thumbnailByteArrayFin = QByteArray()
|
||||
thumbnailImageBuffer = QBuffer(thumbnailByteArrayFin)
|
||||
thumbnailImageBuffer.open(QIODevice.WriteOnly)
|
||||
thumbnailImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||
thumbnailIconImageScaled.save(thumbnailImageBuffer, "PNG")
|
||||
thumbnailImageBuffer.close()
|
||||
except Exception:
|
||||
|
||||
47
Modules/MailDomainReputation/EvaPingUtil.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class EvaPingUtil:
|
||||
name = "Eva PingUtil Email Check"
|
||||
category = "Reputation Check"
|
||||
description = "Check if an email address is disposable, spam, or gibberish."
|
||||
originTypes = {'Email Address', 'Domain'}
|
||||
resultTypes = {'Phrase'}
|
||||
|
||||
parameters = {}
|
||||
|
||||
def resolution(self, entityJsonList, parameters):
|
||||
import requests
|
||||
|
||||
returnResults = []
|
||||
|
||||
for entity in entityJsonList:
|
||||
entityType = entity['Entity Type']
|
||||
if entityType == 'Email Address':
|
||||
email = entity['Email Address']
|
||||
elif entityType == 'Domain':
|
||||
email = f"{entity['Domain Name'].split('.')[0]}@{entity['Domain Name']}"
|
||||
else:
|
||||
continue
|
||||
result = requests.get('https://api.eva.pingutil.com/email?email=' + email).json()
|
||||
disposable = result['data']['disposable']
|
||||
spam = result['data']['spam']
|
||||
gibberish = result['data']['gibberish']
|
||||
if disposable or spam or gibberish:
|
||||
returnResults.append([{'Phrase': 'Poor Reputation: ' + email,
|
||||
'Disposable': str(disposable),
|
||||
'Spam': str(spam),
|
||||
'Gibberish': str(gibberish),
|
||||
'Entity Type': 'Phrase'},
|
||||
{entity['uid']: {'Resolution': 'Eva PingUtil Email Check',
|
||||
'Notes': ''}}])
|
||||
else:
|
||||
returnResults.append([{'Phrase': 'Good Reputation: ' + email,
|
||||
'Disposable': str(disposable),
|
||||
'Spam': str(spam),
|
||||
'Gibberish': str(gibberish),
|
||||
'Entity Type': 'Phrase'},
|
||||
{entity['uid']: {'Resolution': 'Eva PingUtil Email Check',
|
||||
'Notes': ''}}])
|
||||
|
||||
return returnResults
|
||||
@@ -45,7 +45,7 @@ class OpenCorporateCompaniesAddress:
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
|
||||
if parameters['OpenCorporates API Key'] == 'No Key':
|
||||
if parameters['OpenCorporates API Key'].strip() == 'No Key':
|
||||
# Set up parameters
|
||||
data_params = parse.urlencode({
|
||||
'q': entity['Company Name']
|
||||
@@ -70,7 +70,7 @@ class OpenCorporateCompaniesAddress:
|
||||
)
|
||||
# Rate limited to the Starter API rate.
|
||||
time.sleep(0.02)
|
||||
data = r.json
|
||||
data = r.json()
|
||||
|
||||
if r.status_code == 401:
|
||||
return 'Invalid API Key'
|
||||
|
||||
@@ -52,7 +52,7 @@ class OpenCorporateCompanyOfficers:
|
||||
companyCode = entity['Company Number']
|
||||
uid = entity['uid']
|
||||
|
||||
if parameters['OpenCorporates API Key'] == 'No Key':
|
||||
if parameters['OpenCorporates API Key'].strip() == 'No Key':
|
||||
# Perform and process get request
|
||||
try:
|
||||
r = requests.get(f"https://api.opencorporates.com/v0.4/companies/{jurisdictionCode}/{companyCode}")
|
||||
@@ -71,7 +71,7 @@ class OpenCorporateCompanyOfficers:
|
||||
return "Please check your internet connection"
|
||||
# Rate limited to the Starter API rate.
|
||||
time.sleep(80)
|
||||
data = r.json
|
||||
data = r.json()
|
||||
|
||||
if r.status_code == 401:
|
||||
return 'Invalid API Key'
|
||||
|
||||
@@ -47,7 +47,7 @@ class OpenCorporateCompanyResults:
|
||||
for entity in entityJsonList:
|
||||
uid = entity['uid']
|
||||
|
||||
if parameters['OpenCorporates API Key'] == 'No Key':
|
||||
if parameters['OpenCorporates API Key'].strip() == 'No Key':
|
||||
# Set up parameters
|
||||
data_params = parse.urlencode({
|
||||
'q': entity[list(entity)[1]]
|
||||
@@ -75,7 +75,7 @@ class OpenCorporateCompanyResults:
|
||||
return "Please check your internet connection"
|
||||
# Rate limited to the Starter API rate.
|
||||
time.sleep(0.02)
|
||||
data = r.json
|
||||
data = r.json()
|
||||
|
||||
if r.status_code == 401:
|
||||
return 'Invalid API Key'
|
||||
|
||||
@@ -50,7 +50,7 @@ class OpenCorporateOfficerCompanies:
|
||||
if entity['Entity Type'] == 'Open Corporates Officer':
|
||||
primaryField = primaryField.split(' | ')[0] # Nobody has ' | ' in their name - 2022/7/10
|
||||
|
||||
if parameters['OpenCorporates API Key'] == 'No Key':
|
||||
if parameters['OpenCorporates API Key'].strip() == 'No Key':
|
||||
# Set up parameters
|
||||
data_params = parse.urlencode({
|
||||
'q': primaryField
|
||||
@@ -79,7 +79,7 @@ class OpenCorporateOfficerCompanies:
|
||||
# Rate limited to the Starter API rate.
|
||||
time.sleep(0.02)
|
||||
# print(r)
|
||||
data = r.json
|
||||
data = r.json()
|
||||
|
||||
if r.status_code == 401:
|
||||
return 'Invalid API Key'
|
||||
|
||||
@@ -106,7 +106,7 @@ class SteamGroupChecker:
|
||||
thumbnailIconImageScaled = thumbnailIconImageOriginal.scaled(QSize(40, 40))
|
||||
thumbnailByteArrayFin = QByteArray()
|
||||
thumbnailImageBuffer = QBuffer(thumbnailByteArrayFin)
|
||||
thumbnailImageBuffer.open(QIODevice.WriteOnly)
|
||||
thumbnailImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||
thumbnailIconImageScaled.save(thumbnailImageBuffer, "PNG")
|
||||
thumbnailImageBuffer.close()
|
||||
except Exception:
|
||||
|
||||
@@ -41,7 +41,7 @@ class SteamGroupMembersChecker:
|
||||
thumbnailIconImageScaled = thumbnailIconImageOriginal.scaled(QSize(40, 40))
|
||||
thumbnailByteArrayFin = QByteArray()
|
||||
thumbnailImageBuffer = QBuffer(thumbnailByteArrayFin)
|
||||
thumbnailImageBuffer.open(QIODevice.WriteOnly)
|
||||
thumbnailImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||
thumbnailIconImageScaled.save(thumbnailImageBuffer, "PNG")
|
||||
thumbnailImageBuffer.close()
|
||||
except Exception:
|
||||
|
||||
@@ -107,7 +107,7 @@ class SteamUsernameChecker:
|
||||
thumbnailIconImageScaled = thumbnailIconImageOriginal.scaled(QSize(40, 40))
|
||||
thumbnailByteArrayFin = QByteArray()
|
||||
thumbnailImageBuffer = QBuffer(thumbnailByteArrayFin)
|
||||
thumbnailImageBuffer.open(QIODevice.WriteOnly)
|
||||
thumbnailImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||
thumbnailIconImageScaled.save(thumbnailImageBuffer, "PNG")
|
||||
thumbnailImageBuffer.close()
|
||||
except Exception:
|
||||
|
||||
@@ -157,7 +157,7 @@ class TwitterUser:
|
||||
childIconImageScaled = childIconImageOriginal.scaled(QSize(40, 40))
|
||||
childIconByteArrayFin = QByteArray()
|
||||
childImageBuffer = QBuffer(childIconByteArrayFin)
|
||||
childImageBuffer.open(QIODevice.WriteOnly)
|
||||
childImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||
childIconImageScaled.save(childImageBuffer, "PNG")
|
||||
childImageBuffer.close()
|
||||
except Exception:
|
||||
@@ -216,7 +216,7 @@ class TwitterUser:
|
||||
iconImageScaled = iconImageOriginal.scaled(QSize(40, 40))
|
||||
iconByteArrayFin = QByteArray()
|
||||
imageBuffer = QBuffer(iconByteArrayFin)
|
||||
imageBuffer.open(QIODevice.WriteOnly)
|
||||
imageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||
iconImageScaled.save(imageBuffer, "PNG")
|
||||
imageBuffer.close()
|
||||
except Exception:
|
||||
|
||||
@@ -63,7 +63,7 @@ class PinterestUsersSearch:
|
||||
childIconImageScaled = childIconImageOriginal.scaled(QSize(40, 40))
|
||||
childIconByteArrayFin = QByteArray()
|
||||
childImageBuffer = QBuffer(childIconByteArrayFin)
|
||||
childImageBuffer.open(QIODevice.WriteOnly)
|
||||
childImageBuffer.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||
childIconImageScaled.save(childImageBuffer, "PNG")
|
||||
childImageBuffer.close()
|
||||
except Exception:
|
||||
|
||||
@@ -18,6 +18,7 @@ class Whats_My_Name:
|
||||
from concurrent.futures import as_completed
|
||||
from pathlib import Path
|
||||
import json
|
||||
import contextlib
|
||||
from playwright.sync_api import sync_playwright, TimeoutError
|
||||
|
||||
import re
|
||||
@@ -38,10 +39,9 @@ class Whats_My_Name:
|
||||
file = json.load(web_accounts_list)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.firefox.launch()
|
||||
browser = p.chromium.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'
|
||||
viewport={'width': 1920, 'height': 1080}
|
||||
)
|
||||
page = context.new_page()
|
||||
|
||||
@@ -58,51 +58,62 @@ class Whats_My_Name:
|
||||
original_uri = site['uri_check'].replace('{account}', social_field)
|
||||
account_existence_code = site['e_code']
|
||||
account_existence_string = site['e_string']
|
||||
account_missing_string = site['m_string']
|
||||
account_missing_code = site['m_code']
|
||||
requires_javascript = site.get('requires_javascript', False)
|
||||
post_body = site['post_body']
|
||||
if site['valid']:
|
||||
account_existence_string = re.escape(account_existence_string)
|
||||
account_existence_string = re.compile(account_existence_string)
|
||||
account_missing_string = re.escape(account_missing_string)
|
||||
account_missing_string = re.compile(account_missing_string)
|
||||
if requires_javascript:
|
||||
for _ in range(3):
|
||||
try:
|
||||
with contextlib.suppress(TimeoutError):
|
||||
response = page.goto(original_uri, wait_until="networkidle", timeout=10000)
|
||||
status_code = response.status
|
||||
page_source = page.content()
|
||||
if status_code == account_existence_code and \
|
||||
account_existence_string != "" and \
|
||||
len(account_existence_string.findall(page_source)) > 0:
|
||||
return_result.append([{'URL': original_uri,
|
||||
'Entity Type': 'Website'},
|
||||
{uid: {'Resolution': 'Whats My Name Account Match',
|
||||
'Notes': ''}}])
|
||||
len(account_existence_string.findall(page_source)) > 0 and \
|
||||
(len(account_missing_string.findall(page_source)) == 0
|
||||
if account_missing_code == account_existence_code else True):
|
||||
return_result.append(
|
||||
[{'URL': original_uri,
|
||||
'Entity Type': 'Website'},
|
||||
{uid: {'Resolution': 'Whats My Name Account Match',
|
||||
'Notes': ''}}])
|
||||
break
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
else:
|
||||
post_body = site['post_body']
|
||||
if post_body != "":
|
||||
futures[session.post(original_uri, data=post_body, headers=headers,
|
||||
timeout=10, allow_redirects=False)] = \
|
||||
(account_existence_code, account_existence_string)
|
||||
(uid, account_existence_code, account_existence_string,
|
||||
account_missing_string, account_missing_code)
|
||||
else:
|
||||
futures[session.get(original_uri, headers=headers,
|
||||
timeout=10, allow_redirects=False)] = \
|
||||
(account_existence_code, account_existence_string)
|
||||
(uid, account_existence_code, account_existence_string,
|
||||
account_missing_string, account_missing_code)
|
||||
for future in as_completed(futures):
|
||||
account_existence_code = futures[future][0]
|
||||
account_existence_string = futures[future][1]
|
||||
parent_uid = futures[future][0]
|
||||
account_existence_code = futures[future][1]
|
||||
account_existence_string = futures[future][2]
|
||||
account_missing_string = futures[future][3]
|
||||
account_missing_code = futures[future][4]
|
||||
try:
|
||||
first_response = future.result()
|
||||
except RequestException:
|
||||
continue
|
||||
page_source = first_response.text
|
||||
if first_response.status_code == account_existence_code and \
|
||||
account_existence_string != "" and \
|
||||
len(account_existence_string.findall(page_source)) > 0:
|
||||
if first_response.status_code == account_existence_code and account_existence_string != "" and \
|
||||
len(account_existence_string.findall(page_source)) > 0 and \
|
||||
(len(account_missing_string.findall(page_source)) == 0
|
||||
if account_missing_code == account_existence_code else True):
|
||||
return_result.append([{'URL': first_response.url,
|
||||
'Entity Type': 'Website'},
|
||||
{uid: {'Resolution': 'Whats My Name Account Match', 'Notes': ''}}])
|
||||
{parent_uid: {'Resolution': 'Whats My Name Account Match',
|
||||
'Notes': ''}}])
|
||||
page.close()
|
||||
browser.close()
|
||||
return return_result
|
||||
|
||||
@@ -42,9 +42,9 @@ Note that the SFDP graph layout does not function on Windows, as an essential gr
|
||||
Since Version 1.0.0, installers are provided for Windows 11 and Linux (Ubuntu) platforms.
|
||||
|
||||
Download the latest installer for your platform from the Releases page, and run it to install the software:
|
||||
https://github.com/AccentuSoft/LinkScope_Client/releases/tag/v1.0.0
|
||||
https://github.com/AccentuSoft/LinkScope_Client/releases/tag/v1.4.0
|
||||
|
||||
Note: On Ubuntu, you may need to mark the downloaded installer as executable before you can run it. To do this, right-click the installer, and from the drop-down menu, select 'properties'. On the dialog window that pops up, navigate to the 'Permissions' tab, and make sure that 'Allow executing file as program' is checked. You should at this point be able to run the installer by double-clicking it.
|
||||
Note: On Ubuntu, you may need to mark the downloaded installer as executable before you can run it. To do this, right-click the installer, and from the drop-down menu, select 'properties'. On the dialog window that pops up, navigate to the 'Permissions' tab, and make sure that 'Allow executing file as program' is checked. You should at this point be able to run the installer by double-clicking it. If double-clicking the installer does not start it, you can also launch the installer through a terminal.
|
||||
|
||||
### Running from source
|
||||
One could also clone the repository and run the software as-is.
|
||||
@@ -71,6 +71,8 @@ The Wiki page contains a manual on using the tool, available here: https://githu
|
||||
|
||||
We have an introductory blog post that explains the basics of using the tool, available here: https://accentusoft.com/tutorials/first-steps-with-linkscope-client/
|
||||
|
||||
We also have some videos demonstrating how to use the software on our YouTube channel: https://www.youtube.com/channel/UC8h9Vde1OdezdC2cJ1nEUcw
|
||||
|
||||
## Extending the software
|
||||
LinkScope was built from the ground up to be modular! In this repository's wiki, there are instructions on how to create your own modules, which can contain custom Entities and Resolutions. There is also an example module in the Modules directory that can act as a template, and has a verbose description of most things that a module creator should need to consider.
|
||||
|
||||
@@ -78,7 +80,7 @@ LinkScope was built from the ground up to be modular! In this repository's wiki,
|
||||
Warnings and best practices on using the software:
|
||||
|
||||
### Do NOT use Resolutions obtained by untrusted sources.
|
||||
Resolutions are essentially Python code that ingests information from various data sources. Make sure that the people providing you with code to run are trustworthy, and that you inspect all the modules you use before installing them. Pre-compiled binaries on systems without Python installed are not able to install new resolutions, so compiling custom versions of the client to suit your investigators' needs should effectively mitigate this risk.
|
||||
Resolutions are essentially Python code that ingests information from various data sources. Make sure that the people providing you with code to run are trustworthy, and that you inspect all the modules you use before installing them.
|
||||
|
||||
### Do NOT interact with unsafe resources without sufficient protection measures.
|
||||
Some resources that an investigator may access through the software may pose a risk to the investigator and/or the assets used during the investigation. Some examples include, but are not limited to: Sites that host malicious code, torrents for malware, or forums that contain illegal materials. Handling the risk of investigating targets and materials that may be harmful to the investigator and/or the equipment used during the investigation is completely up to the investigator.
|
||||
|
||||
29
Resources/Icons/Aircraft.svg
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="40" height="40" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<clipPath id="b">
|
||||
<path d="m777 483h200.86v83h-200.86z"/>
|
||||
</clipPath>
|
||||
<clipPath id="a">
|
||||
<path d="m222.14 552h605.86v403h-605.86z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path d="m1200 600c0 214.36-114.36 412.44-300 519.62-185.64 107.18-414.36 107.18-600 0-185.64-107.18-300-305.26-300-519.62s114.36-412.44 300-519.62c185.64-107.18 414.36-107.18 600 0 185.64 107.18 300 305.26 300 519.62" fill="#758bcc"/>
|
||||
<path d="m380.3 671.18 111.94-74.859-82.703-168.71c-5.6641-11.551-5.1055-25.043 1.4844-36.09l9.6016-16.086c3.4102-5.7188 1.9922-13.074-3.2969-17.113-5.4336-4.1484-13.234-3.3789-17.758 1.75l-23.266 26.379c-11.484 13.027-17.246 30.426-15.797 47.734z" fill="#fff"/>
|
||||
<path d="m706.25 560.55-12.074 5.8906c-3.0078 1.4688-4.5586 4.8789-3.6797 8.1133l11.312 41.762c0.54688 2.0195 1.9531 3.6719 3.8594 4.5391s4.0742 0.83203 5.957-0.082031l12.07-5.8906c3.0078-1.4688 4.5586-4.8789 3.6836-8.1133l-11.312-41.762c-1.4141-4.4844-4.793-6.0547-9.8164-4.457z" fill="#fff"/>
|
||||
<g clip-path="url(#b)">
|
||||
<path d="m977.7 531.55c-0.21094-0.86719-0.97266-2.9805-3.5117-3.8672l-127.82-44.242c-1.3398 0-2.6562 0.44141-3.7461 1.2852l-63.176 48.922c-2.8945 2.2422-2.4297 5.3555-2.2188 6.2578 0.21094 0.89844 1.1797 3.8945 4.7656 4.6094 71.266 14.219 123.37 21.676 142.96 20.449 21.539-1.3477 39.828-16.137 51.379-28.305 1.875-1.9805 1.582-4.2344 1.3711-5.1094z" fill="#fff"/>
|
||||
</g>
|
||||
<path d="m345.03 828.57c4.7539-2.2266 8.3242-6.3984 9.793-11.445 1.4766-5.0703 0.69531-10.535-2.1406-14.988-4.9336-7.7539-15.016-10.609-23.445-6.6484-13.547 6.3633-25.719 9.5859-36.184 9.5859-7.1836 0-13.344-1.4805-18.832-4.5195-4.3828-2.4297-8.1445-5.5-11.281-9.1523-7.207 10.82-13.523 21.809-18.879 32.703 3.7891 3.1406 7.9492 5.9766 12.426 8.4609 23.973 13.293 54.492 11.938 88.543-3.9961z" fill="#fff"/>
|
||||
<path d="m717.69 439.84c4.5391-4.7266 8.4883-9.207 11.852-13.391l-45.363-24.973c-5.5039-3.0273-12.016-2.9297-17.422 0.26562-5.4062 3.1953-8.6367 8.8555-8.6367 15.137v68.512l54.879-36.703c0.66406-3.2344 2.2422-6.2969 4.6914-8.8477z" fill="#fff"/>
|
||||
<path d="m725.51 454.49c0.34375 0.80078 1.7227 3.418 5.168 3.418h17.645c11.328 0 22.48-3.0234 32.262-8.7461l47.414-27.758c5.4492-3.1914 9.0781-8.6094 9.9531-14.863l19.949-142.66c0.79687-5.6953-1.3867-11.262-5.8398-14.895-2.9727-2.4297-6.5898-3.6914-10.281-3.6914-1.8398 0-3.6992 0.3125-5.5078 0.95312-28.91 10.258-48.09 21.965-55.453 33.848-10 16.137-14.871 51.27-19.168 82.27-3.4023 24.539-6.3398 45.73-11.551 55.766-4.4023 8.4766-12.297 18.668-23.477 30.297-2.375 2.4766-1.4531 5.2695-1.1133 6.0664z" fill="#fff"/>
|
||||
<path d="m337.29 873.02 106.24-82.207-7.1133-26.258c-2.4258-8.957 1.8672-18.414 10.207-22.484l12.07-5.8906c5.2109-2.5469 11.23-2.6289 16.512-0.23047 5.2812 2.3984 9.1797 6.9844 10.695 12.578l2.125 7.8516 15.699-12.148c-1.332-4.8008-2.0742-10.129-2.0742-15.777 0-21.809 10.953-38.891 24.938-38.891 9.5664 0 17.711 7.9922 21.906 20.023l15.148-11.723c-1.3164-4.7773-2.0469-10.07-2.0469-15.684 0-21.809 10.953-38.891 24.938-38.891 9.5391 0 17.668 7.9531 21.871 19.934l15.156-11.727c-1.3008-4.75-2.0195-10.012-2.0195-15.59 0-21.809 10.953-38.891 24.938-38.891 9.5156 0 17.625 7.918 21.84 19.844l17.195-13.305-6.9766-25.758c-2.4258-8.957 1.8672-18.414 10.207-22.484l12.074-5.8906c5.2109-2.5469 11.23-2.6289 16.512-0.23047 5.2812 2.3984 9.1797 6.9844 10.695 12.582l1.9922 7.3477 35.012-27.09c-1.3555-6.918 1.2148-13.828 6.832-18.176l63.172-48.922c4.9805-3.8594 11.5-4.918 17.445-2.8438l31.234 10.902c5.375-13.211 9.8086-27.273 13.188-41.879 0.82422-3.5586-0.015625-7.25-2.3008-10.125-2.3125-2.9102-5.7695-4.5781-9.4844-4.5781h-43.656c-2.0938 2.1914-4.5078 4.1094-7.2031 5.6875l-47.414 27.758c-11.676 6.832-24.992 10.445-38.52 10.445h-17.645c-6.3477 0-11.945-3.1406-15.207-8.3516l-375.02 250.79c-28.027 18.742-51.57 42.641-70.102 68.016 2.5078 3.7266 5.75 6.6875 9.8633 8.9648 3.6055 1.9961 7.8047 2.9688 12.828 2.9688 8.6289 0 19.031-2.8281 30.914-8.4102 14.055-6.6055 30.891-1.7852 39.164 11.207 4.75 7.4609 6.0547 16.609 3.5859 25.102-2.4648 8.4727-8.4531 15.469-16.434 19.207-20.23 9.4688-39.516 14.27-57.316 14.27-15.434 0-29.73-3.582-42.488-10.652-4.1719-2.3125-8.082-4.9258-11.754-7.7539-3.7617 8.6719-6.9023 17.215-9.3984 25.492l13.328 10.805c27.609 22.391 66.531 22.828 94.645 1.0703z" fill="#fff"/>
|
||||
<path d="m633.94 635.91c0 15.168 6.625 26.504 12.551 26.504 5.9219 0 12.551-11.336 12.551-26.504s-6.625-26.504-12.551-26.504c-5.9219 0-12.551 11.336-12.551 26.504z" fill="#fff"/>
|
||||
<path d="m470.09 747.23c-0.92188-0.41797-1.9062-0.62891-2.8906-0.62891-1.0469 0-2.0977 0.23828-3.0664 0.71094l-12.074 5.8906c-3.0078 1.4688-4.5586 4.8789-3.6797 8.1133l11.312 41.762c0.54688 2.0195 1.9531 3.6719 3.8594 4.5391 1.9062 0.86719 4.0742 0.83594 5.957-0.082032l12.07-5.8945c3.0078-1.4688 4.5547-4.8789 3.6797-8.1094l-11.312-41.762c-0.54688-2.0195-1.9531-3.6719-3.8555-4.5391z" fill="#fff"/>
|
||||
<path d="m956.19 749.45-41.23 7.582c-12.195 2.2422-24.531 2.0898-36.668-0.46484l-211.45-44.414-137.91 111.36 327.45 8.7773c15.086 0.41016 29.898-3.5547 42.785-11.449l68.336-41.871c7.2109-4.418 9.7695-13.734 5.8203-21.215-3.2969-6.2422-10.176-9.5781-17.129-8.3047z" fill="#fff"/>
|
||||
<g clip-path="url(#a)">
|
||||
<path d="m827.35 565.7c-17.535-3.1094-34.316-6.3477-47.781-9.0312-3.1094-0.62109-5.8984-1.9688-8.207-3.875l-37.84 29.277 5.8164 21.465c2.4258 8.957-1.8672 18.414-10.207 22.484l-12.074 5.8906c-5.6445 1.7422-11.16 1.9102-16.512 0.23047-5.2812-2.3984-9.1797-6.9844-10.695-12.578l-0.82812-3.0586-17.852 13.812c0.16016 1.8242 0.25 3.6875 0.25 5.5898 0 21.809-10.953 38.891-24.938 38.891-7.082 0-13.383-4.3867-17.879-11.539l-17.367 13.438c0.15625 1.7891 0.24219 3.6172 0.24219 5.4805 0 21.809-10.953 38.891-24.938 38.891-7.0547 0-13.336-4.3516-17.832-11.461l-17.406 13.469c0.14844 1.7539 0.23047 3.543 0.23047 5.3711 0 21.809-10.953 38.891-24.938 38.891-7.0312 0-13.293-4.3203-17.785-11.387l-17.277 13.367 5.6797 20.965c2.4258 8.957-1.8672 18.414-10.207 22.484l-12.07 5.8945c-5.9961 1.5938-11.547 1.8164-16.508 0.23047-5.2812-2.3984-9.1797-6.9844-10.699-12.582l-0.69141-2.5586-102.16 79.055c-16.059 12.43-35.156 18.625-54.234 18.625-19.746 0-39.469-6.6367-55.797-19.871l-8.9961-7.293c-1.4492 6.3945-2.4961 12.582-3.0859 18.453-2.293 22.902 1.8398 40.398 11.961 50.59 34.668 34.93 145.45-15.59 228.2-82.422l350.77-283.26c4.7148-3.793 9.2656-7.8008 13.652-11.93z" fill="#fff"/>
|
||||
</g>
|
||||
<path d="m573.99 682.18c0 15.168 6.625 26.504 12.551 26.504 5.9219 0 12.551-11.336 12.551-26.504 0-15.168-6.625-26.504-12.551-26.504s-12.551 11.336-12.551 26.504z" fill="#fff"/>
|
||||
<path d="m514.04 728.45c0 15.168 6.625 26.504 12.551 26.504 5.9219 0 12.551-11.336 12.551-26.504s-6.625-26.504-12.551-26.504c-5.9219 0-12.551 11.336-12.551 26.504z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
4
Resources/Icons/Banner_Cyan.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="37" height="37" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m797.97 905.21c4.3125-1.7852 8-4.8125 10.594-8.6953 2.5977-3.8867 3.9805-8.4492 3.9805-13.121v-566.79c0-6.2656-2.4883-12.27-6.918-16.699-4.4297-4.4297-10.434-6.918-16.699-6.918h-377.86c-6.2656 0-12.27 2.4883-16.699 6.918-4.4297 4.4297-6.918 10.434-6.918 16.699v566.79c0 6.2617 2.4883 12.27 6.918 16.699 4.4258 4.4297 10.434 6.918 16.699 6.918 6.2617-0.003907 12.27-2.4922 16.695-6.918l172.23-172.23 172.23 172.23c3.3008 3.3008 7.5078 5.5508 12.09 6.4648 4.582 0.91016 9.332 0.44141 13.648-1.3477z" fill="#12b0fb"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 673 B |
4
Resources/Icons/Banner_Orange.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="37" height="37" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m797.97 905.21c4.3125-1.7852 8-4.8125 10.594-8.6953 2.5977-3.8867 3.9805-8.4492 3.9805-13.121v-566.79c0-6.2656-2.4883-12.27-6.918-16.699-4.4297-4.4297-10.434-6.918-16.699-6.918h-377.86c-6.2656 0-12.27 2.4883-16.699 6.918-4.4297 4.4297-6.918 10.434-6.918 16.699v566.79c0 6.2617 2.4883 12.27 6.918 16.699 4.4258 4.4297 10.434 6.918 16.699 6.918 6.2617-0.003907 12.27-2.4922 16.695-6.918l172.23-172.23 172.23 172.23c3.3008 3.3008 7.5078 5.5508 12.09 6.4648 4.582 0.91016 9.332 0.44141 13.648-1.3477z" fill="#ff814a"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 673 B |
4
Resources/Icons/Banner_Red.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="37" height="37" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m797.97 905.21c4.3125-1.7852 8-4.8125 10.594-8.6953 2.5977-3.8867 3.9805-8.4492 3.9805-13.121v-566.79c0-6.2656-2.4883-12.27-6.918-16.699-4.4297-4.4297-10.434-6.918-16.699-6.918h-377.86c-6.2656 0-12.27 2.4883-16.699 6.918-4.4297 4.4297-6.918 10.434-6.918 16.699v566.79c0 6.2617 2.4883 12.27 6.918 16.699 4.4258 4.4297 10.434 6.918 16.699 6.918 6.2617-0.003907 12.27-2.4922 16.695-6.918l172.23-172.23 172.23 172.23c3.3008 3.3008 7.5078 5.5508 12.09 6.4648 4.582 0.91016 9.332 0.44141 13.648-1.3477z" fill="#ff001b"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 673 B |
29
Resources/Icons/Sanctioned_Aircraft.svg
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="40" height="40" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<clipPath id="b">
|
||||
<path d="m777 483h200.86v83h-200.86z"/>
|
||||
</clipPath>
|
||||
<clipPath id="a">
|
||||
<path d="m222.14 552h605.86v403h-605.86z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path d="m1200 600c0 214.36-114.36 412.44-300 519.62-185.64 107.18-414.36 107.18-600 0-185.64-107.18-300-305.26-300-519.62s114.36-412.44 300-519.62c185.64-107.18 414.36-107.18 600 0 185.64 107.18 300 305.26 300 519.62"/>
|
||||
<path d="m380.3 671.18 111.94-74.859-82.703-168.71c-5.6641-11.551-5.1055-25.043 1.4844-36.09l9.6016-16.086c3.4102-5.7188 1.9922-13.074-3.2969-17.113-5.4336-4.1484-13.234-3.3789-17.758 1.75l-23.266 26.379c-11.484 13.027-17.246 30.426-15.797 47.734z" fill="#fff"/>
|
||||
<path d="m706.25 560.55-12.074 5.8906c-3.0078 1.4688-4.5586 4.8789-3.6797 8.1133l11.312 41.762c0.54688 2.0195 1.9531 3.6719 3.8594 4.5391s4.0742 0.83203 5.957-0.082031l12.07-5.8906c3.0078-1.4688 4.5586-4.8789 3.6836-8.1133l-11.312-41.762c-1.4141-4.4844-4.793-6.0547-9.8164-4.457z" fill="#fff"/>
|
||||
<g clip-path="url(#b)">
|
||||
<path d="m977.7 531.55c-0.21094-0.86719-0.97266-2.9805-3.5117-3.8672l-127.82-44.242c-1.3398 0-2.6562 0.44141-3.7461 1.2852l-63.176 48.922c-2.8945 2.2422-2.4297 5.3555-2.2188 6.2578 0.21094 0.89844 1.1797 3.8945 4.7656 4.6094 71.266 14.219 123.37 21.676 142.96 20.449 21.539-1.3477 39.828-16.137 51.379-28.305 1.875-1.9805 1.582-4.2344 1.3711-5.1094z" fill="#fff"/>
|
||||
</g>
|
||||
<path d="m345.03 828.57c4.7539-2.2266 8.3242-6.3984 9.793-11.445 1.4766-5.0703 0.69531-10.535-2.1406-14.988-4.9336-7.7539-15.016-10.609-23.445-6.6484-13.547 6.3633-25.719 9.5859-36.184 9.5859-7.1836 0-13.344-1.4805-18.832-4.5195-4.3828-2.4297-8.1445-5.5-11.281-9.1523-7.207 10.82-13.523 21.809-18.879 32.703 3.7891 3.1406 7.9492 5.9766 12.426 8.4609 23.973 13.293 54.492 11.938 88.543-3.9961z" fill="#fff"/>
|
||||
<path d="m717.69 439.84c4.5391-4.7266 8.4883-9.207 11.852-13.391l-45.363-24.973c-5.5039-3.0273-12.016-2.9297-17.422 0.26562-5.4062 3.1953-8.6367 8.8555-8.6367 15.137v68.512l54.879-36.703c0.66406-3.2344 2.2422-6.2969 4.6914-8.8477z" fill="#fff"/>
|
||||
<path d="m725.51 454.49c0.34375 0.80078 1.7227 3.418 5.168 3.418h17.645c11.328 0 22.48-3.0234 32.262-8.7461l47.414-27.758c5.4492-3.1914 9.0781-8.6094 9.9531-14.863l19.949-142.66c0.79687-5.6953-1.3867-11.262-5.8398-14.895-2.9727-2.4297-6.5898-3.6914-10.281-3.6914-1.8398 0-3.6992 0.3125-5.5078 0.95312-28.91 10.258-48.09 21.965-55.453 33.848-10 16.137-14.871 51.27-19.168 82.27-3.4023 24.539-6.3398 45.73-11.551 55.766-4.4023 8.4766-12.297 18.668-23.477 30.297-2.375 2.4766-1.4531 5.2695-1.1133 6.0664z" fill="#fff"/>
|
||||
<path d="m337.29 873.02 106.24-82.207-7.1133-26.258c-2.4258-8.957 1.8672-18.414 10.207-22.484l12.07-5.8906c5.2109-2.5469 11.23-2.6289 16.512-0.23047 5.2812 2.3984 9.1797 6.9844 10.695 12.578l2.125 7.8516 15.699-12.148c-1.332-4.8008-2.0742-10.129-2.0742-15.777 0-21.809 10.953-38.891 24.938-38.891 9.5664 0 17.711 7.9922 21.906 20.023l15.148-11.723c-1.3164-4.7773-2.0469-10.07-2.0469-15.684 0-21.809 10.953-38.891 24.938-38.891 9.5391 0 17.668 7.9531 21.871 19.934l15.156-11.727c-1.3008-4.75-2.0195-10.012-2.0195-15.59 0-21.809 10.953-38.891 24.938-38.891 9.5156 0 17.625 7.918 21.84 19.844l17.195-13.305-6.9766-25.758c-2.4258-8.957 1.8672-18.414 10.207-22.484l12.074-5.8906c5.2109-2.5469 11.23-2.6289 16.512-0.23047 5.2812 2.3984 9.1797 6.9844 10.695 12.582l1.9922 7.3477 35.012-27.09c-1.3555-6.918 1.2148-13.828 6.832-18.176l63.172-48.922c4.9805-3.8594 11.5-4.918 17.445-2.8438l31.234 10.902c5.375-13.211 9.8086-27.273 13.188-41.879 0.82422-3.5586-0.015625-7.25-2.3008-10.125-2.3125-2.9102-5.7695-4.5781-9.4844-4.5781h-43.656c-2.0938 2.1914-4.5078 4.1094-7.2031 5.6875l-47.414 27.758c-11.676 6.832-24.992 10.445-38.52 10.445h-17.645c-6.3477 0-11.945-3.1406-15.207-8.3516l-375.02 250.79c-28.027 18.742-51.57 42.641-70.102 68.016 2.5078 3.7266 5.75 6.6875 9.8633 8.9648 3.6055 1.9961 7.8047 2.9688 12.828 2.9688 8.6289 0 19.031-2.8281 30.914-8.4102 14.055-6.6055 30.891-1.7852 39.164 11.207 4.75 7.4609 6.0547 16.609 3.5859 25.102-2.4648 8.4727-8.4531 15.469-16.434 19.207-20.23 9.4688-39.516 14.27-57.316 14.27-15.434 0-29.73-3.582-42.488-10.652-4.1719-2.3125-8.082-4.9258-11.754-7.7539-3.7617 8.6719-6.9023 17.215-9.3984 25.492l13.328 10.805c27.609 22.391 66.531 22.828 94.645 1.0703z" fill="#fff"/>
|
||||
<path d="m633.94 635.91c0 15.168 6.625 26.504 12.551 26.504 5.9219 0 12.551-11.336 12.551-26.504s-6.625-26.504-12.551-26.504c-5.9219 0-12.551 11.336-12.551 26.504z" fill="#fff"/>
|
||||
<path d="m470.09 747.23c-0.92188-0.41797-1.9062-0.62891-2.8906-0.62891-1.0469 0-2.0977 0.23828-3.0664 0.71094l-12.074 5.8906c-3.0078 1.4688-4.5586 4.8789-3.6797 8.1133l11.312 41.762c0.54688 2.0195 1.9531 3.6719 3.8594 4.5391 1.9062 0.86719 4.0742 0.83594 5.957-0.082032l12.07-5.8945c3.0078-1.4688 4.5547-4.8789 3.6797-8.1094l-11.312-41.762c-0.54688-2.0195-1.9531-3.6719-3.8555-4.5391z" fill="#fff"/>
|
||||
<path d="m956.19 749.45-41.23 7.582c-12.195 2.2422-24.531 2.0898-36.668-0.46484l-211.45-44.414-137.91 111.36 327.45 8.7773c15.086 0.41016 29.898-3.5547 42.785-11.449l68.336-41.871c7.2109-4.418 9.7695-13.734 5.8203-21.215-3.2969-6.2422-10.176-9.5781-17.129-8.3047z" fill="#fff"/>
|
||||
<g clip-path="url(#a)">
|
||||
<path d="m827.35 565.7c-17.535-3.1094-34.316-6.3477-47.781-9.0312-3.1094-0.62109-5.8984-1.9688-8.207-3.875l-37.84 29.277 5.8164 21.465c2.4258 8.957-1.8672 18.414-10.207 22.484l-12.074 5.8906c-5.6445 1.7422-11.16 1.9102-16.512 0.23047-5.2812-2.3984-9.1797-6.9844-10.695-12.578l-0.82812-3.0586-17.852 13.812c0.16016 1.8242 0.25 3.6875 0.25 5.5898 0 21.809-10.953 38.891-24.938 38.891-7.082 0-13.383-4.3867-17.879-11.539l-17.367 13.438c0.15625 1.7891 0.24219 3.6172 0.24219 5.4805 0 21.809-10.953 38.891-24.938 38.891-7.0547 0-13.336-4.3516-17.832-11.461l-17.406 13.469c0.14844 1.7539 0.23047 3.543 0.23047 5.3711 0 21.809-10.953 38.891-24.938 38.891-7.0312 0-13.293-4.3203-17.785-11.387l-17.277 13.367 5.6797 20.965c2.4258 8.957-1.8672 18.414-10.207 22.484l-12.07 5.8945c-5.9961 1.5938-11.547 1.8164-16.508 0.23047-5.2812-2.3984-9.1797-6.9844-10.699-12.582l-0.69141-2.5586-102.16 79.055c-16.059 12.43-35.156 18.625-54.234 18.625-19.746 0-39.469-6.6367-55.797-19.871l-8.9961-7.293c-1.4492 6.3945-2.4961 12.582-3.0859 18.453-2.293 22.902 1.8398 40.398 11.961 50.59 34.668 34.93 145.45-15.59 228.2-82.422l350.77-283.26c4.7148-3.793 9.2656-7.8008 13.652-11.93z" fill="#fff"/>
|
||||
</g>
|
||||
<path d="m573.99 682.18c0 15.168 6.625 26.504 12.551 26.504 5.9219 0 12.551-11.336 12.551-26.504 0-15.168-6.625-26.504-12.551-26.504s-12.551 11.336-12.551 26.504z" fill="#fff"/>
|
||||
<path d="m514.04 728.45c0 15.168 6.625 26.504 12.551 26.504 5.9219 0 12.551-11.336 12.551-26.504s-6.625-26.504-12.551-26.504c-5.9219 0-12.551 11.336-12.551 26.504z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
31
Resources/Icons/Sanctioned_Vessel.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="40" height="40" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<clipPath id="d">
|
||||
<path d="m222.14 765h755.71v107h-755.71z"/>
|
||||
</clipPath>
|
||||
<clipPath id="c">
|
||||
<path d="m222.14 847h755.71v130.86h-755.71z"/>
|
||||
</clipPath>
|
||||
<clipPath id="b">
|
||||
<path d="m222.14 670h755.71v119h-755.71z"/>
|
||||
</clipPath>
|
||||
<clipPath id="a">
|
||||
<path d="m416 222.14h368v311.86h-368z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path d="m1200 600c0 214.36-114.36 412.44-300 519.62-185.64 107.18-414.36 107.18-600 0-185.64-107.18-300-305.26-300-519.62s114.36-412.44 300-519.62c185.64-107.18 414.36-107.18 600 0 185.64 107.18 300 305.26 300 519.62"/>
|
||||
<g clip-path="url(#d)">
|
||||
<path d="m966.05 777.12c-19.594-0.50391-38.855 5.1328-55.09 16.117-20.195 12.207-43.344 18.656-66.945 18.656-23.598 0-46.746-6.4531-66.941-18.656-16.578-10.156-35.641-15.531-55.082-15.531-19.441 0-38.504 5.375-55.082 15.531-20.25 12.043-43.371 18.402-66.934 18.398-23.559 0-46.684-6.3555-66.934-18.398-16.691-9.8281-35.707-15.008-55.074-15.008-19.371 0-38.387 5.1836-55.078 15.008-20.258 12.02-43.375 18.363-66.93 18.363-23.555 0-46.676-6.3438-66.93-18.363-16.23-10.988-35.488-16.621-55.078-16.117-6.5234 0-11.809-5.2852-11.809-11.809v82.656c0-6.5195 5.2852-11.805 11.809-11.805 23.75-0.47266 47.082 6.2617 66.934 19.309 16.227 10.984 35.484 16.617 55.074 16.113 19.59 0.50391 38.848-5.1289 55.078-16.113 19.848-13.047 43.184-19.781 66.934-19.309 23.746-0.46875 47.078 6.2617 66.926 19.309 16.23 10.984 35.488 16.617 55.082 16.113 19.59 0.50391 38.848-5.1289 55.078-16.113 20.047-12.617 43.25-19.309 66.934-19.309 23.688 0 46.891 6.6914 66.938 19.309 16.445 10.523 35.562 16.113 55.09 16.113 19.523 0 38.641-5.5898 55.086-16.113 19.855-13.047 43.191-19.781 66.945-19.309 6.5195 0 11.809 5.2852 11.809 11.805v-82.656c0 6.5234-5.2891 11.809-11.809 11.809z" fill="#fff"/>
|
||||
</g>
|
||||
<g clip-path="url(#c)">
|
||||
<path d="m966.05 859.78c-19.594-0.50391-38.855 5.1328-55.09 16.117-20.051 12.613-43.254 19.309-66.941 19.309s-46.895-6.6953-66.945-19.309c-16.441-10.523-35.559-16.117-55.082-16.117-19.52 0-38.637 5.5938-55.082 16.117-19.848 13.047-43.184 19.777-66.93 19.309-23.75 0.46875-47.086-6.2617-66.934-19.309-16.23-10.984-35.484-16.621-55.074-16.117-19.594-0.50391-38.852 5.1328-55.078 16.117-19.852 13.047-43.188 19.777-66.934 19.309-23.75 0.46875-47.082-6.2656-66.93-19.309-16.23-10.984-35.488-16.621-55.078-16.117-6.5234 0-11.809-5.2852-11.809-11.809v82.656c0 6.5234 5.2852 11.809 11.809 11.809 19.59-0.50391 38.848 5.1328 55.078 16.117 19.848 13.043 43.18 19.777 66.93 19.309 23.746 0.46875 47.082-6.2617 66.934-19.309 16.227-10.984 35.484-16.621 55.078-16.117 19.59-0.50391 38.844 5.1328 55.074 16.117 19.848 13.047 43.184 19.777 66.934 19.309 23.746 0.46875 47.082-6.2617 66.93-19.309 16.445-10.523 35.562-16.117 55.082-16.117 19.523 0 38.641 5.5938 55.082 16.117 20.051 12.613 43.258 19.309 66.945 19.309s46.891-6.6953 66.941-19.309c16.234-10.984 35.496-16.621 55.09-16.117 6.5195 0 11.809-5.2852 11.809-11.809v-82.656c0 6.5234-5.2891 11.809-11.809 11.809z" fill="#fff"/>
|
||||
</g>
|
||||
<g clip-path="url(#b)">
|
||||
<path d="m966.05 670.85c-23.75-0.47266-47.09 6.2578-66.941 19.305-16.609 10.074-35.664 15.398-55.09 15.398s-38.477-5.3242-55.086-15.395c-20.254-12.039-43.379-18.391-66.938-18.391s-46.684 6.3516-66.938 18.387c-16.609 10.062-35.656 15.383-55.078 15.383-19.418 0-38.469-5.3164-55.078-15.379-20.18-12.238-43.332-18.711-66.93-18.711-23.602 0-46.75 6.4727-66.93 18.707-16.625 10.023-35.668 15.32-55.078 15.32-19.414 0-38.457-5.2969-55.078-15.316-19.852-13.047-43.184-19.781-66.934-19.309-6.5234 0-11.809 5.2852-11.809 11.805v82.656c0-6.5195 5.2852-11.805 11.809-11.805 23.75-0.47266 47.082 6.2617 66.934 19.309 16.566 10.176 35.633 15.566 55.078 15.566 19.441 0 38.508-5.3906 55.074-15.57 20.242-12.066 43.367-18.438 66.934-18.438 23.562 0 46.688 6.3711 66.93 18.441 16.566 10.18 35.633 15.57 55.078 15.57 19.445 0 38.512-5.3906 55.078-15.574 20.246-12.055 43.375-18.422 66.938-18.422 23.562 0.003906 46.691 6.3672 66.938 18.426 16.617 10.039 35.668 15.348 55.086 15.348s38.469-5.3086 55.09-15.352c19.852-13.047 43.191-19.777 66.941-19.305 6.5195 0 11.809 5.2852 11.809 11.805v-82.656c0-6.5195-5.2891-11.805-11.809-11.805z" fill="#fff"/>
|
||||
</g>
|
||||
<g clip-path="url(#a)">
|
||||
<path d="m602.84 493.32 180.19 39.852v-104.38c0-6.2656-2.4883-12.273-6.918-16.699-4.4258-4.4297-10.434-6.918-16.699-6.918h-11.809v-35.422c0.003907-6.2656-2.4844-12.273-6.9141-16.703-4.4297-4.4297-10.438-6.9141-16.703-6.9141h-5.9023v-23.617c0-6.5195-5.2852-11.809-11.809-11.809-6.5195 0-11.809 5.2891-11.809 11.809v23.617h-47.23v-64.945c-0.019531-6.5117-5.2969-11.789-11.809-11.809h-23.617v-23.613h11.809c6.5234 0 11.809-5.2891 11.809-11.809 0-6.5234-5.2852-11.809-11.809-11.809h-47.23c-6.5234 0-11.809 5.2852-11.809 11.809 0 6.5195 5.2852 11.809 11.809 11.809h11.809v23.613h-23.617c-6.5156 0.019531-11.789 5.2969-11.809 11.809v64.945h-47.23v-23.617c0-6.5195-5.2891-11.809-11.809-11.809-6.5234 0-11.809 5.2891-11.809 11.809v23.617h-5.9062c-6.2617 0-12.27 2.4844-16.699 6.9141-4.4297 4.4297-6.918 10.438-6.9141 16.703v35.426l-11.809-0.003906c-6.2656 0-12.273 2.4883-16.703 6.918-4.4258 4.4258-6.9141 10.434-6.9141 16.699v104.5l180.78-39.969c1.6758-0.35547 3.4023-0.35547 5.0781 0zm79.82-29.105h-35.422v-35.426h35.426zm-206.64-94.465h247.97v35.426l-247.97-0.003906zm76.754 94.465h-35.426v-35.426h35.426zm23.617-35.426h47.23v35.426h-47.23z" fill="#fff"/>
|
||||
</g>
|
||||
<path d="m403.57 667.37c23.461-13.203 50.008-19.941 76.926-19.523 26.918 0.41406 53.242 7.9648 76.285 21.883 12.996 7.9922 27.957 12.223 43.215 12.223 15.262 0 30.219-4.2305 43.219-12.223 46.984-27.844 105.19-28.762 153.03-2.418 0.22266 0.035156 0.44141 0.09375 0.65234 0.17578 14.207-28.809 21.582-60.508 21.547-92.633-0.003907-5.5508-3.8516-10.359-9.2695-11.574l-208.88-46.344-209.47 46.344c-5.418 1.2148-9.2656 6.0234-9.2695 11.574-0.027344 32.121 7.3438 63.82 21.547 92.633 0.14453-0.082031 0.30859-0.12109 0.47266-0.11719zm256.47-70.73c1.4023-6.3633 7.6797-10.398 14.051-9.0352l43.336 9.3867v0.003907c5.8906 1.2891 9.8672 6.8008 9.2305 12.801-0.63281 5.9961-5.6797 10.555-11.711 10.578-0.85547-0.011719-1.707-0.10938-2.5391-0.29688l-43.336-9.3867c-6.3633-1.4023-10.398-7.6797-9.0312-14.051zm-179.36 0.35547 43.336-9.3867-0.003906-0.003907c3.0664-0.67969 6.2812-0.10937 8.9258 1.5859 2.6445 1.6914 4.5078 4.3672 5.1758 7.4375 0.66797 3.0703 0.085937 6.2812-1.6211 8.918-1.7031 2.6406-4.3867 4.4922-7.4609 5.1445l-43.336 9.3867c-0.83203 0.1875-1.6836 0.28516-2.5391 0.29688-6.0312-0.023438-11.074-4.582-11.711-10.578-0.63672-6 3.3398-11.512 9.2305-12.801z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
7
Resources/Icons/Spreadsheet.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="40" height="40" version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path d="m100 50c0 27.613-22.387 50-50 50s-50-22.387-50-50 22.387-50 50-50 50 22.387 50 50" fill="#007335"/>
|
||||
<path d="m30.992 74.109v-48.789c0-1.1367 1.1367-1.1367 1.1367-1.1367h26.098l10.211 10.215v39.715c0 1.1328-1.1328 1.1328-1.1328 1.1328h-35.176s-1.1367 0-1.1367-1.1367zm1.7031-1.6328v-45.523c0-1.0664 1.0352-1.0664 1.0352-1.0664h22.793v9.0781c0 1.1367 1.1367 1.1367 1.1367 1.1367h9.0781v36.375c0 1.0703-1.0352 1.0703-1.0352 1.0703h-31.973s-1.0352 0-1.0352-1.0703zm2.2695-31.84v29.504h29.504v-29.504zm1.7031 5.1094v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8047 0v2.2695h5.6758v-2.2695zm-20.422 3.4023v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8047 0v2.2695h5.6758v-2.2695zm-20.422 3.4062v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8047 0v2.2695h5.6758v-2.2695zm-20.422 3.4023v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8047 0v2.2695h5.6758v-2.2695zm-20.422 3.4023v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8047 0v2.2695h5.6758v-2.2695zm-20.422 3.4062v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8047 0v2.2695h5.6758v-2.2695zm-20.422 3.4023v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8086 0v2.2695h5.6719v-2.2695zm6.8047 0v2.2695h5.6758v-2.2695z" fill="#fff" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
31
Resources/Icons/Vessel.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="40" height="40" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<clipPath id="d">
|
||||
<path d="m222.14 765h755.71v107h-755.71z"/>
|
||||
</clipPath>
|
||||
<clipPath id="c">
|
||||
<path d="m222.14 847h755.71v130.86h-755.71z"/>
|
||||
</clipPath>
|
||||
<clipPath id="b">
|
||||
<path d="m222.14 670h755.71v119h-755.71z"/>
|
||||
</clipPath>
|
||||
<clipPath id="a">
|
||||
<path d="m416 222.14h368v311.86h-368z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path d="m1200 600c0 214.36-114.36 412.44-300 519.62-185.64 107.18-414.36 107.18-600 0-185.64-107.18-300-305.26-300-519.62s114.36-412.44 300-519.62c185.64-107.18 414.36-107.18 600 0 185.64 107.18 300 305.26 300 519.62" fill="#042fad"/>
|
||||
<g clip-path="url(#d)">
|
||||
<path d="m966.05 777.12c-19.594-0.50391-38.855 5.1328-55.09 16.117-20.195 12.207-43.344 18.656-66.945 18.656-23.598 0-46.746-6.4531-66.941-18.656-16.578-10.156-35.641-15.531-55.082-15.531-19.441 0-38.504 5.375-55.082 15.531-20.25 12.043-43.371 18.402-66.934 18.398-23.559 0-46.684-6.3555-66.934-18.398-16.691-9.8281-35.707-15.008-55.074-15.008-19.371 0-38.387 5.1836-55.078 15.008-20.258 12.02-43.375 18.363-66.93 18.363-23.555 0-46.676-6.3438-66.93-18.363-16.23-10.988-35.488-16.621-55.078-16.117-6.5234 0-11.809-5.2852-11.809-11.809v82.656c0-6.5195 5.2852-11.805 11.809-11.805 23.75-0.47266 47.082 6.2617 66.934 19.309 16.227 10.984 35.484 16.617 55.074 16.113 19.59 0.50391 38.848-5.1289 55.078-16.113 19.848-13.047 43.184-19.781 66.934-19.309 23.746-0.46875 47.078 6.2617 66.926 19.309 16.23 10.984 35.488 16.617 55.082 16.113 19.59 0.50391 38.848-5.1289 55.078-16.113 20.047-12.617 43.25-19.309 66.934-19.309 23.688 0 46.891 6.6914 66.938 19.309 16.445 10.523 35.562 16.113 55.09 16.113 19.523 0 38.641-5.5898 55.086-16.113 19.855-13.047 43.191-19.781 66.945-19.309 6.5195 0 11.809 5.2852 11.809 11.805v-82.656c0 6.5234-5.2891 11.809-11.809 11.809z" fill="#fff"/>
|
||||
</g>
|
||||
<g clip-path="url(#c)">
|
||||
<path d="m966.05 859.78c-19.594-0.50391-38.855 5.1328-55.09 16.117-20.051 12.613-43.254 19.309-66.941 19.309s-46.895-6.6953-66.945-19.309c-16.441-10.523-35.559-16.117-55.082-16.117-19.52 0-38.637 5.5938-55.082 16.117-19.848 13.047-43.184 19.777-66.93 19.309-23.75 0.46875-47.086-6.2617-66.934-19.309-16.23-10.984-35.484-16.621-55.074-16.117-19.594-0.50391-38.852 5.1328-55.078 16.117-19.852 13.047-43.188 19.777-66.934 19.309-23.75 0.46875-47.082-6.2656-66.93-19.309-16.23-10.984-35.488-16.621-55.078-16.117-6.5234 0-11.809-5.2852-11.809-11.809v82.656c0 6.5234 5.2852 11.809 11.809 11.809 19.59-0.50391 38.848 5.1328 55.078 16.117 19.848 13.043 43.18 19.777 66.93 19.309 23.746 0.46875 47.082-6.2617 66.934-19.309 16.227-10.984 35.484-16.621 55.078-16.117 19.59-0.50391 38.844 5.1328 55.074 16.117 19.848 13.047 43.184 19.777 66.934 19.309 23.746 0.46875 47.082-6.2617 66.93-19.309 16.445-10.523 35.562-16.117 55.082-16.117 19.523 0 38.641 5.5938 55.082 16.117 20.051 12.613 43.258 19.309 66.945 19.309s46.891-6.6953 66.941-19.309c16.234-10.984 35.496-16.621 55.09-16.117 6.5195 0 11.809-5.2852 11.809-11.809v-82.656c0 6.5234-5.2891 11.809-11.809 11.809z" fill="#fff"/>
|
||||
</g>
|
||||
<g clip-path="url(#b)">
|
||||
<path d="m966.05 670.85c-23.75-0.47266-47.09 6.2578-66.941 19.305-16.609 10.074-35.664 15.398-55.09 15.398s-38.477-5.3242-55.086-15.395c-20.254-12.039-43.379-18.391-66.938-18.391s-46.684 6.3516-66.938 18.387c-16.609 10.062-35.656 15.383-55.078 15.383-19.418 0-38.469-5.3164-55.078-15.379-20.18-12.238-43.332-18.711-66.93-18.711-23.602 0-46.75 6.4727-66.93 18.707-16.625 10.023-35.668 15.32-55.078 15.32-19.414 0-38.457-5.2969-55.078-15.316-19.852-13.047-43.184-19.781-66.934-19.309-6.5234 0-11.809 5.2852-11.809 11.805v82.656c0-6.5195 5.2852-11.805 11.809-11.805 23.75-0.47266 47.082 6.2617 66.934 19.309 16.566 10.176 35.633 15.566 55.078 15.566 19.441 0 38.508-5.3906 55.074-15.57 20.242-12.066 43.367-18.438 66.934-18.438 23.562 0 46.688 6.3711 66.93 18.441 16.566 10.18 35.633 15.57 55.078 15.57 19.445 0 38.512-5.3906 55.078-15.574 20.246-12.055 43.375-18.422 66.938-18.422 23.562 0.003906 46.691 6.3672 66.938 18.426 16.617 10.039 35.668 15.348 55.086 15.348s38.469-5.3086 55.09-15.352c19.852-13.047 43.191-19.777 66.941-19.305 6.5195 0 11.809 5.2852 11.809 11.805v-82.656c0-6.5195-5.2891-11.805-11.809-11.805z" fill="#fff"/>
|
||||
</g>
|
||||
<g clip-path="url(#a)">
|
||||
<path d="m602.84 493.32 180.19 39.852v-104.38c0-6.2656-2.4883-12.273-6.918-16.699-4.4258-4.4297-10.434-6.918-16.699-6.918h-11.809v-35.422c0.003907-6.2656-2.4844-12.273-6.9141-16.703-4.4297-4.4297-10.438-6.9141-16.703-6.9141h-5.9023v-23.617c0-6.5195-5.2852-11.809-11.809-11.809-6.5195 0-11.809 5.2891-11.809 11.809v23.617h-47.23v-64.945c-0.019531-6.5117-5.2969-11.789-11.809-11.809h-23.617v-23.613h11.809c6.5234 0 11.809-5.2891 11.809-11.809 0-6.5234-5.2852-11.809-11.809-11.809h-47.23c-6.5234 0-11.809 5.2852-11.809 11.809 0 6.5195 5.2852 11.809 11.809 11.809h11.809v23.613h-23.617c-6.5156 0.019531-11.789 5.2969-11.809 11.809v64.945h-47.23v-23.617c0-6.5195-5.2891-11.809-11.809-11.809-6.5234 0-11.809 5.2891-11.809 11.809v23.617h-5.9062c-6.2617 0-12.27 2.4844-16.699 6.9141-4.4297 4.4297-6.918 10.438-6.9141 16.703v35.426l-11.809-0.003906c-6.2656 0-12.273 2.4883-16.703 6.918-4.4258 4.4258-6.9141 10.434-6.9141 16.699v104.5l180.78-39.969c1.6758-0.35547 3.4023-0.35547 5.0781 0zm79.82-29.105h-35.422v-35.426h35.426zm-206.64-94.465h247.97v35.426l-247.97-0.003906zm76.754 94.465h-35.426v-35.426h35.426zm23.617-35.426h47.23v35.426h-47.23z" fill="#fff"/>
|
||||
</g>
|
||||
<path d="m403.57 667.37c23.461-13.203 50.008-19.941 76.926-19.523 26.918 0.41406 53.242 7.9648 76.285 21.883 12.996 7.9922 27.957 12.223 43.215 12.223 15.262 0 30.219-4.2305 43.219-12.223 46.984-27.844 105.19-28.762 153.03-2.418 0.22266 0.035156 0.44141 0.09375 0.65234 0.17578 14.207-28.809 21.582-60.508 21.547-92.633-0.003907-5.5508-3.8516-10.359-9.2695-11.574l-208.88-46.344-209.47 46.344c-5.418 1.2148-9.2656 6.0234-9.2695 11.574-0.027344 32.121 7.3438 63.82 21.547 92.633 0.14453-0.082031 0.30859-0.12109 0.47266-0.11719zm256.47-70.73c1.4023-6.3633 7.6797-10.398 14.051-9.0352l43.336 9.3867v0.003907c5.8906 1.2891 9.8672 6.8008 9.2305 12.801-0.63281 5.9961-5.6797 10.555-11.711 10.578-0.85547-0.011719-1.707-0.10938-2.5391-0.29688l-43.336-9.3867c-6.3633-1.4023-10.398-7.6797-9.0312-14.051zm-179.36 0.35547 43.336-9.3867-0.003906-0.003907c3.0664-0.67969 6.2812-0.10937 8.9258 1.5859 2.6445 1.6914 4.5078 4.3672 5.1758 7.4375 0.66797 3.0703 0.085937 6.2812-1.6211 8.918-1.7031 2.6406-4.3867 4.4922-7.4609 5.1445l-43.336 9.3867c-0.83203 0.1875-1.6836 0.28516-2.5391 0.29688-6.0312-0.023438-11.074-4.582-11.711-10.578-0.63672-6 3.3398-11.512 9.2305-12.801z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
18
build.sh
@@ -4,18 +4,18 @@
|
||||
# to a new directory along with this file.
|
||||
# Then, run the script.
|
||||
|
||||
PYTHON_VER=3.9
|
||||
PYTHON_VER=3.10
|
||||
|
||||
python${PYTHON_VER} -m venv buildEnv
|
||||
|
||||
source buildEnv/bin/activate
|
||||
|
||||
# orderedset package installed for compile time performance.
|
||||
python${PYTHON_VER} -m pip install --upgrade wheel pip nuitka orderedset
|
||||
python${PYTHON_VER} -m pip install --upgrade wheel pip nuitka ordered-set
|
||||
|
||||
python${PYTHON_VER} -m pip cache purge
|
||||
|
||||
python${PYTHON_VER} -m pip install -r requirements.txt
|
||||
python${PYTHON_VER} -m pip install --upgrade -r requirements.txt
|
||||
|
||||
# Stop snscrape from messing with directories that will not exist in the final build.
|
||||
echo "" > "buildEnv/lib/python${PYTHON_VER}/site-packages/snscrape/modules/__init__.py"
|
||||
@@ -26,10 +26,10 @@ FIREFOX_VER=$(python -c "from pathlib import Path;x=Path(\"buildEnv/lib/python${
|
||||
|
||||
python${PYTHON_VER} -m nuitka --follow-imports --standalone --noinclude-pytest-mode=nofollow \
|
||||
--noinclude-setuptools-mode=nofollow --noinclude-custom-mode=setuptools:error --noinclude-IPython-mode=nofollow \
|
||||
--enable-plugin=pyside6 --enable-plugin=numpy --enable-plugin=trio --assume-yes-for-downloads --remove-output \
|
||||
--noinclude-unittest-mode=nofollow --enable-plugin=pyside6 --enable-plugin=trio --assume-yes-for-downloads --remove-output \
|
||||
--disable-console --include-data-dir="Resources=Resources" --include-plugin-directory=Modules --include-package=Core \
|
||||
--include-data-dir="Core/Entities=Core/Entities" --include-data-dir="Core/Resolutions/Core=Core/Resolutions/Core" \
|
||||
--include-data-dir="Modules=Modules" --warn-unusual-code --show-modules --include-data-files="Icon.ico=Icon.ico" \
|
||||
--include-data-dir="Core/Entities=Core/Entities" \
|
||||
--warn-unusual-code --show-modules --include-data-files="Icon.ico=Icon.ico" \
|
||||
--linux-icon="Icon.ico" \
|
||||
--include-package-data=playwright \
|
||||
--include-package-data=folium --include-package-data=branca \
|
||||
@@ -43,7 +43,11 @@ python${PYTHON_VER} -m nuitka --follow-imports --standalone --noinclude-pytest-m
|
||||
--include-package=jellyfish \
|
||||
--include-package=ipwhois \
|
||||
--include-package=tweepy \
|
||||
--include-data-dir="buildEnv/lib/python${PYTHON_VER}/site-packages/playwright/driver/package/.local-browsers/firefox-${FIREFOX_VER}/firefox=playwright/driver/package/.local-browsers/firefox-${FIREFOX_VER}/firefox" \
|
||||
LinkScope.py
|
||||
|
||||
# We need the code, dll & etc files to be present.
|
||||
cp -r "buildEnv/lib/python${PYTHON_VER}/site-packages/playwright/driver/package/.local-browsers/firefox-${FIREFOX_VER}/firefox" "LinkScope.dist/playwright/driver/package/.local-browsers/firefox-${FIREFOX_VER}"
|
||||
cp -r Modules/ LinkScope.dist
|
||||
cp -r Core/Resolutions LinkScope.dist/Core
|
||||
|
||||
deactivate
|
||||
|
||||
18
buildWin.bat
@@ -8,17 +8,17 @@
|
||||
:: PatchMagicWin.py to a new directory along with this file.
|
||||
:: Then, run the script.
|
||||
|
||||
SET PYTHON_VER=3.9
|
||||
SET PYTHON_VER=3.10
|
||||
|
||||
python -m venv buildEnv
|
||||
|
||||
call buildEnv\Scripts\activate.bat
|
||||
|
||||
python -m pip install --upgrade wheel pip nuitka orderedset
|
||||
python -m pip install --upgrade wheel pip nuitka ordered-set
|
||||
|
||||
python -m pip cache purge
|
||||
|
||||
python -m pip install -r requirements.txt
|
||||
python -m pip install --upgrade -r requirements.txt
|
||||
|
||||
:: Patch magic library with our own binaries
|
||||
python PatchMagicWin.py
|
||||
@@ -34,10 +34,10 @@ FOR /F "usebackq" %%L in (`python -c "from pathlib import Path;x=Path('buildEnv\
|
||||
|
||||
python -m nuitka --follow-imports --standalone --noinclude-pytest-mode=nofollow --noinclude-setuptools-mode=nofollow ^
|
||||
--noinclude-custom-mode=setuptools:error --noinclude-IPython-mode=nofollow --enable-plugin=pyside6 ^
|
||||
--enable-plugin=numpy --enable-plugin=trio --assume-yes-for-downloads --remove-output --disable-console ^
|
||||
--noinclude-unittest-mode=nofollow --enable-plugin=trio --assume-yes-for-downloads --remove-output --disable-console ^
|
||||
--include-data-dir="Resources=Resources" --include-plugin-directory=Modules --include-package=Core ^
|
||||
--include-data-dir="Core\Entities=Core\Entities" --include-data-dir="Core\Resolutions\Core=Core\Resolutions\Core" ^
|
||||
--include-data-dir="Modules=Modules" --warn-unusual-code --show-modules --include-data-files="Icon.ico=Icon.ico" ^
|
||||
--include-data-dir="Core\Entities=Core\Entities" ^
|
||||
--warn-unusual-code --show-modules --include-data-files="Icon.ico=Icon.ico" ^
|
||||
--windows-icon-from-ico=".\Icon.ico" --include-data-dir="magic=magic" --windows-company-name=AccentuSoft ^
|
||||
--windows-product-name="LinkScope Client" --windows-product-version="1.3.8.0" ^
|
||||
--windows-file-description="LinkScope Client Software" ^
|
||||
@@ -53,7 +53,11 @@ python -m nuitka --follow-imports --standalone --noinclude-pytest-mode=nofollow
|
||||
--include-package=jellyfish ^
|
||||
--include-package=ipwhois ^
|
||||
--include-package=tweepy ^
|
||||
--include-data-dir="buildEnv\Lib\site-packages\playwright\driver\package\.local-browsers\firefox-%FIREFOX_VER%\firefox=playwright\driver\package\.local-browsers\firefox-%FIREFOX_VER%\firefox" ^
|
||||
".\LinkScope.py"
|
||||
|
||||
:: We need the code, dll & etc files to be present.
|
||||
xcopy "buildEnv\Lib\site-packages\playwright\driver\package\.local-browsers\firefox-%FIREFOX_VER%\firefox" "LinkScope.dist\playwright\driver\package\.local-browsers\firefox-%FIREFOX_VER%" /S /E /Y
|
||||
xcopy Modules\ LinkScope.dist /S /E /Y
|
||||
xcopy Core\Resolutions LinkScope.dist\Core /S /E /Y
|
||||
|
||||
call buildEnv\Scripts\deactivate.bat
|
||||
@@ -1,4 +1,5 @@
|
||||
PySide6
|
||||
pyqtdarktheme
|
||||
|
||||
|
||||
# For python-magic, the following packages may be required:
|
||||
@@ -28,6 +29,10 @@ reportlab
|
||||
pandas
|
||||
svglib
|
||||
lxml
|
||||
# For opening spreadsheets
|
||||
odfpy
|
||||
openpyxl
|
||||
xlrd # For old Excel formats
|
||||
# Speeds up parsing of webpages by bs4
|
||||
cchardet
|
||||
bs4
|
||||
@@ -42,7 +47,7 @@ playwright
|
||||
requests
|
||||
shodan
|
||||
docx2python
|
||||
PyPDF2
|
||||
pypdf
|
||||
beautifulsoup4
|
||||
python-Wappalyzer
|
||||
vtapi3
|
||||
|
||||