Skip to content

Commit

Permalink
support for sending replies
Browse files Browse the repository at this point in the history
  • Loading branch information
heartsucker committed Feb 4, 2019
1 parent d13f574 commit d704511
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 8 deletions.
85 changes: 79 additions & 6 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QListWidget, QLabel, QWidget, QListWidgetItem, QHBoxLayout, \
QPushButton, QVBoxLayout, QLineEdit, QScrollArea, QDialog, QAction, QMenu, \
QMessageBox, QToolButton, QSizePolicy
QMessageBox, QToolButton, QSizePolicy, QTextEdit
from uuid import uuid4

from securedrop_client.db import Source
from securedrop_client.logic import Client
Expand Down Expand Up @@ -503,7 +504,7 @@ def __init__(self, message_id: str, text: str, update_signal) -> None:
def _update_text(self, message_id: str, 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.
signal matches the message_id of this speech bubble.
"""
if message_id == self.message_id:
self.message.setText(html.escape(text, quote=False))
Expand Down Expand Up @@ -566,14 +567,45 @@ class ReplyWidget(ConversationWidget):
Represents a reply to a source.
"""

def __init__(self, message_id: str, message: str, update_signal) -> None:
def __init__(
self,
message_id: str,
message: str,
update_signal,
message_succeeded_signal,
message_failed_signal,
) -> None:
super().__init__(message_id,
message,
update_signal,
align="right")
self.message_id = message_id
self.setStyleSheet("""
background-color: #2299EE;
""")
message_succeeded_signal.connect(self._on_reply_success)
message_failed_signal.connect(self._on_reply_failure)

@pyqtSlot(str)
def _on_reply_success(self, message_id: str) -> None:
"""
Conditionally update this ReplyWidget's state if and only if the message_id of the emitted
signal matches the message_id of this widget.
"""
if message_id == self.message_id:
logger.debug('Message {} succeeded'.format(message_id))

@pyqtSlot(str)
def _on_reply_failure(self, message_id: str) -> None:
"""
Conditionally update this ReplyWidget's state if and only if the message_id of the emitted
signal matches the message_id of this widget.
"""
if message_id == self.message_id:
logger.debug('Message {} failed'.format(message_id))
self.setStyleSheet("""
background-color: #FF3E3C;
""")


class FileWidget(QWidget):
Expand Down Expand Up @@ -723,7 +755,12 @@ def add_reply(self, message_id: str, reply: str, files=None) -> None:
Add a reply from a journalist.
"""
self.conversation_layout.addWidget(
ReplyWidget(message_id, reply, self.controller.reply_sync.reply_downloaded))
ReplyWidget(message_id,
reply,
self.controller.reply_sync.reply_downloaded,
self.controller.reply_succeeded,
self.controller.reply_failed,
))


class SourceConversationWrapper(QWidget):
Expand All @@ -734,14 +771,50 @@ class SourceConversationWrapper(QWidget):

def __init__(self, source: Source, sdc_home: str, controller: Client, parent=None) -> None:
super().__init__(parent)
self.source = source
self.controller = controller
self.layout = QVBoxLayout()
self.setLayout(self.layout)

self.conversation = ConversationView(source, sdc_home, controller, parent=self)
self.source_profile = SourceProfileShortWidget(source, controller)
self.conversation = ConversationView(self.source, sdc_home, self.controller, parent=self)
self.source_profile = SourceProfileShortWidget(self.source, self.controller)
self.reply_box = ReplyBoxWidget(self)

self.layout.addWidget(self.source_profile)
self.layout.addWidget(self.conversation)
self.layout.addWidget(self.reply_box)

def send_reply(self, message: str) -> None:
msg_uuid = str(uuid4())
self.conversation.add_reply(msg_uuid, message)
self.controller.send_reply(self.source.uuid, msg_uuid, message)


class ReplyBoxWidget(QWidget):
"""
A textbox where a journalist can enter a reply.
"""

def __init__(self, conversation: SourceConversationWrapper) -> None:
super().__init__()
self.conversation = conversation

self.text_edit = QTextEdit()

self.send_button = QPushButton('Send')
self.send_button.clicked.connect(self.send_reply)

layout = QHBoxLayout()
layout.addWidget(self.text_edit)
layout.addWidget(self.send_button)
self.setLayout(layout)

def send_reply(self) -> None:
msg = self.text_edit.toPlainText().strip()
if not msg:
return
self.conversation.send_reply(msg)
self.text_edit.clear()


class DeleteSourceAction(QAction):
Expand Down
57 changes: 55 additions & 2 deletions securedrop_client/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import os
import arrow
import logging
import os
import sdclientapi
import shutil
import arrow
import traceback
import uuid
from securedrop_client import storage
from securedrop_client import db
Expand Down Expand Up @@ -89,6 +90,18 @@ class Client(QObject):

sync_events = pyqtSignal(str)

"""
Signal that notifies that a reply was accepted by the server. Emits the reply's UUID as a
string.
"""
reply_succeeded = pyqtSignal(str)

"""
Signal that notifies that a reply failed to be accepted by the server. Emits the reply's UUID
as a string.
"""
reply_failed = pyqtSignal(str)

def __init__(self, hostname, gui, session,
home: str, proxy: bool = True) -> None:
"""
Expand Down Expand Up @@ -642,3 +655,43 @@ def delete_source(self, source):
self._on_delete_action_timeout,
source
)

def send_reply(self, source_uuid: str, msg_uuid: str, message: str) -> None:
sdk_source = sdclientapi.Source(uuid=source_uuid)

try:
encrypted_reply = self.gpg.encrypt_to_source(source_uuid, message)
except Exception:
tb = traceback.format_exc()
logger.error('Failed to encrypt to source {}:\n'.format(source_uuid, tb))
self.reply_failed.emit(msg_uuid)
else:
self.call_api(
self.api.reply_source,
self._on_reply_complete,
self._on_reply_timeout,
sdk_source,
encrypted_reply,
msg_uuid,
current_object=(source_uuid, msg_uuid),
)

def _on_reply_complete(self, result, current_object: (str, str)) -> None:
source_uuid, reply_uuid = current_object
source = self.session.query(db.Source).filter_by(uuid=source_uuid).one()
if isinstance(result, sdclientapi.Reply):
reply_db_object = db.Reply(
uuid=result.uuid,
source_id=source.id,
journalist_id=self.api.token['journalist_uuid'],
filename=result.filename,
)
self.session.add(reply_db_object)
self.session.commit()
self.reply_succeeded.emit(reply_uuid)
else:
self.reply_failed.emit(reply_uuid)

def _on_reply_timeout(self, current_object: (str, str)) -> None:
_, reply_uuid = current_object
self.reply_failed.emit(reply_uuid)

0 comments on commit d704511

Please sign in to comment.