Skip to content

Commit

Permalink
attempt to conect signals and slots
Browse files Browse the repository at this point in the history
  • Loading branch information
heartsucker committed Jan 7, 2019
1 parent cc8f6a4 commit 105c5b8
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 45 deletions.
2 changes: 1 addition & 1 deletion securedrop_client/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def show_conversation_for(self, source):
conversation_container = self.conversations.get(source.uuid, None)

if conversation_container is None:
conversation = ConversationView(source, self.sdc_home, parent=self)
conversation = ConversationView(source, self.sdc_home, self.controller, parent=self)
conversation.setup(self.controller)

conversation_container = QWidget()
Expand Down
62 changes: 44 additions & 18 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
import logging
import arrow
import html
from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QListWidget, QLabel, QWidget, QListWidgetItem, QHBoxLayout, \
QPushButton, QVBoxLayout, QLineEdit, QScrollArea, QDialog, QAction, QMenu, \
QMessageBox, QToolButton
from uuid import UUID

from securedrop_client.logic import Client
from securedrop_client.resources import load_svg, load_image
from securedrop_client.storage import get_data
from securedrop_client.utils import humanize_filesize
Expand Down Expand Up @@ -485,31 +487,48 @@ class SpeechBubble(QWidget):

css = "padding: 10px; border: 1px solid #999; border-radius: 20px;"

def __init__(self, text):
def __init__(self, message_id: UUID, text: str, update_signal) -> None:
super().__init__()
self.message_id = message_id

layout = QVBoxLayout()
self.setLayout(layout)

message = QLabel(html.escape(text, quote=False))
message.setWordWrap(True)
self.message = QLabel(html.escape(text, quote=False))
self.message.setWordWrap(True)

layout.addWidget(message)
layout.addWidget(self.message)

update_signal.connect(self.__update_text)

@pyqtSlot(UUID, str)
def __update_text(self, message_id: UUID, text: str) -> None:
"""
Conditionally update this SpeechBubble's text if and only if the message_id of the emitted
signal matche the message_id of this speech bubble.
"""
if message_id == self.message_id:
self.message.setText(html.escape(text, quote=False))


class ConversationWidget(QWidget):
"""
Draws a message onto the screen.
"""

def __init__(self, message, align):
def __init__(self,
message_id: UUID,
message: str,
update_signal,
align: str) -> None:
"""
Initialise with the message to display and some notion of which side
of the conversation ("left" or "right" [anything else]) to which the
widget should belong.
"""
super().__init__()
layout = QHBoxLayout()
label = SpeechBubble(message)
label = SpeechBubble(message_id, message, update_signal)

if align is not "left":
# Float right...
Expand All @@ -534,8 +553,11 @@ class MessageWidget(ConversationWidget):
Represents an incoming message from the source.
"""

def __init__(self, message):
super().__init__(message, align="left")
def __init__(self, message_id: UUID, message: str, controller: Client) -> None:
super().__init__(message_id,
message,
controller.message_sync.message_downloaded,
align="left")
self.setStyleSheet("""
background-color: #EEE;
""")
Expand All @@ -546,8 +568,11 @@ class ReplyWidget(ConversationWidget):
Represents a reply to a source.
"""

def __init__(self, message):
super().__init__(message, align="right")
def __init__(self, message_id: UUID, message: str, controller: Client) -> None:
super().__init__(message_id,
message,
controller.reply_sync.reply_downloaded,
align="right")
self.setStyleSheet("""
background-color: #2299EE;
""")
Expand Down Expand Up @@ -613,10 +638,11 @@ class ConversationView(QWidget):
Renders a conversation.
"""

def __init__(self, source_db_object, sdc_home: str, parent=None):
def __init__(self, source_db_object, sdc_home: str, controller, parent=None):
super().__init__(parent)
self.source = source_db_object
self.sdc_home = sdc_home
self.controller = controller

self.container = QWidget()
self.conversation_layout = QVBoxLayout()
Expand Down Expand Up @@ -667,9 +693,9 @@ def add_item_content_or(self, adder, item, default):
Private helper function to add correct message to conversation widgets
"""
if item.is_downloaded is False:
adder(default)
adder(item.uuid, default)
else:
adder(get_data(self.sdc_home, item.filename))
adder(item.uuid, get_data(self.sdc_home, item.filename))

def setup(self, controller):
"""
Expand All @@ -692,17 +718,17 @@ def move_to_bottom(self, min_val, max_val):
"""
self.scroll.verticalScrollBar().setValue(max_val)

def add_message(self, message):
def add_message(self, message_id: UUID, message: str) -> None:
"""
Add a message from the source.
"""
self.conversation_layout.addWidget(MessageWidget(message))
self.conversation_layout.addWidget(MessageWidget(message_id, message, self.controller))

def add_reply(self, reply, files=None):
def add_reply(self, message_id: UUID, reply: str, files=None) -> None:
"""
Add a reply from a journalist.
"""
self.conversation_layout.addWidget(ReplyWidget(reply))
self.conversation_layout.addWidget(ReplyWidget(message_id, reply, self.controller))


class DeleteSourceAction(QAction):
Expand Down
55 changes: 33 additions & 22 deletions securedrop_client/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,22 +96,42 @@ def __init__(self, hostname, gui, session,
various other layers of the application: the location of the SecureDrop
proxy, the user interface and SqlAlchemy local storage respectively.
"""

check_dir_permissions(home)

super().__init__()
self.hostname = hostname # Location of the SecureDrop server.
self.gui = gui # Reference to the UI window.
self.api = None # Reference to the API for secure drop proxy.
self.session = session # Reference to the SqlAlchemy session.
self.message_thread = None # thread responsible for fetching messages
self.reply_thread = None # thread responsible for fetching replies
self.home = home # used for finding DB in sync thread
self.api_threads = {} # Contains active threads calling the API.
self.sync_flag = os.path.join(home, 'sync_flag')
self.data_dir = os.path.join(self.home, 'data') # File data.
self.timer = None # call timeout timer

# used for finding DB in sync thread
self.home = home

# boolean flag for whether or not the client is operating behind a proxy
self.proxy = proxy

# Location of the SecureDrop server.
self.hostname = hostname

# Reference to the UI window.
self.gui = gui

# Reference to the API for secure drop proxy.
self.api = None
# Contains active threads calling the API.
self.api_threads = {}

# Reference to the SqlAlchemy session.
self.session = session

# thread responsible for fetching messages
self.message_thread = None
self.message_sync = MessageSync(self.api, self.home, self.proxy)

# thread responsible for fetching replies
self.reply_thread = None
self.reply_sync = ReplySync(self.api, self.home, self.proxy)

self.sync_flag = os.path.join(home, 'sync_flag')

# File data.
self.data_dir = os.path.join(self.home, 'data')

self.gpg = GpgHelper(home, proxy)

def setup(self):
Expand Down Expand Up @@ -143,13 +163,6 @@ def setup(self):
self.sync_update.timeout.connect(self.sync_api)
self.sync_update.start(1000 * 60 * 5) # every 5 minutes.

# Use a QTimer to update the current conversation view such
# that as downloads/decryption occur, the messages and replies
# populate the view.
self.conv_view_update = QTimer()
self.conv_view_update.timeout.connect(self.update_conversation_views)
self.conv_view_update.start(1000 * 6) # every 6 seconds

def call_api(self, function, callback, timeout, *args, current_object=None,
**kwargs):
"""
Expand Down Expand Up @@ -233,7 +246,6 @@ def start_message_thread(self):
"""
if not self.message_thread:
self.message_thread = QThread()
self.message_sync = MessageSync(self.api, self.home, self.proxy)
self.message_sync.moveToThread(self.message_thread)
self.message_thread.started.connect(self.message_sync.run)
self.message_thread.start()
Expand All @@ -246,7 +258,6 @@ def start_reply_thread(self):
"""
if not self.reply_thread:
self.reply_thread = QThread()
self.reply_sync = ReplySync(self.api, self.home, self.proxy)
self.reply_sync.moveToThread(self.reply_thread)
self.reply_thread.started.connect(self.reply_sync.run)
self.reply_thread.start()
Expand Down
23 changes: 19 additions & 4 deletions securedrop_client/message_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@
import logging
import sdclientapi.sdlocalobjects as sdkobjects

from PyQt5.QtCore import QObject
from PyQt5.QtCore import QObject, pyqtSignal
from securedrop_client import storage
from securedrop_client.crypto import GpgHelper
from securedrop_client.db import make_engine

from securedrop_client.storage import get_data
from sqlalchemy.orm import sessionmaker
from uuid import UUID


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -58,6 +59,12 @@ class MessageSync(APISyncObject):
Runs in the background, finding messages to download and downloading them.
"""

"""
Signal emitted notifying that a message has been downloaded. The signal is a tuple of
(UUID, str) containing the message's UUID and the content of the message.
"""
message_downloaded = pyqtSignal([UUID, str])

def __init__(self, api, home, is_qubes):
super().__init__(api, home, is_qubes)

Expand All @@ -79,6 +86,8 @@ def run(self, loop=True):
db_submission,
self.api.download_submission,
storage.mark_file_as_downloaded)
self.message_downloaded.emit(db_submission.uuid,
get_data(self.home, db_submission.filename))

except Exception as e:
logger.critical(
Expand All @@ -96,6 +105,12 @@ class ReplySync(APISyncObject):
Runs in the background, finding replies to download and downloading them.
"""

"""
Signal emitted notifying that a reply has been downloaded. The signal is a tuple of
(UUID, str) containing the message's UUID and the content of the reply.
"""
reply_downloaded = pyqtSignal([UUID, str])

def __init__(self, api, home, is_qubes):
super().__init__(api, home, is_qubes)

Expand All @@ -105,7 +120,6 @@ def run(self, loop=True):

for db_reply in replies:
try:

# the API wants API objects. here in the client,
# we have client objects. let's take care of that
# here
Expand All @@ -121,7 +135,8 @@ def run(self, loop=True):
db_reply,
self.api.download_reply,
storage.mark_reply_as_downloaded)

self.reply_downloaded.emit(db_reply.uuid,
get_data(self.home, db_reply.filename))
except Exception as e:
logger.critical(
"Exception while downloading reply! {}".format(e)
Expand Down

0 comments on commit 105c5b8

Please sign in to comment.