diff --git a/resources/main-window.ui b/resources/main-window.ui index f9cf913f..58e34f4f 100644 --- a/resources/main-window.ui +++ b/resources/main-window.ui @@ -18,6 +18,11 @@ False xsi-window-close-symbolic + + True + False + xsi-mail-send + True False @@ -700,9 +705,51 @@ False True - 2 + 1 + + + True + False + 6 + + + 40 + True + True + never + external + out + + + True + True + word-char + + + + + True + True + 0 + + + + + True + True + False + image4 + + + False + True + 1 + + + + False diff --git a/resources/op-item.ui b/resources/op-item.ui index 6936f6ea..c0ef1733 100644 --- a/resources/op-item.ui +++ b/resources/op-item.ui @@ -51,6 +51,12 @@ center xsi-list-remove-symbolic + + True + False + center + xsi-edit-copy-symbolic + True False @@ -230,6 +236,32 @@ 2 + + + True + False + vertical + + + True + False + True + True + word-char + 80 + 0 + + + True + True + 0 + + + + + text-message + + True @@ -386,6 +418,21 @@ 8 + + + True + True + True + Copy message + center + image11 + + + False + False + 9 + + diff --git a/src/notifications.py b/src/notifications.py index cc2a7090..f4a64c6d 100644 --- a/src/notifications.py +++ b/src/notifications.py @@ -224,3 +224,34 @@ def _notification_response(self, action, variant, op): app = Gio.Application.get_default() app.lookup_action("notification-response").disconnect_by_func(self._notification_response) + +class TextMessageNotification(): + def __init__(self, op): + self.op = op + self.send_notification() + + @misc._idle + def send_notification(self): + if prefs.get_show_notifications(): + notification = Gio.Notification.new(_("New message from %s") % self.op.sender_name) + notification.set_body(self.op.message) + notification.set_icon(Gio.ThemedIcon(name="org.x.Warpinator-symbolic")) + notification.set_priority(Gio.NotificationPriority.URGENT) + + notification.add_button(_("Copy"), "app.notification-response::copy") + notification.set_default_action("app.notification-response::focus") + + app = Gio.Application.get_default() + app.lookup_action("notification-response").connect("activate", self._notification_response, self.op) + app.send_notification(self.op.sender, notification) + + def _notification_response(self, action, variant, op): + response = variant.unpack() + + if response == "copy": + op.copy_message() + else: + op.focus() + + app = Gio.Application.get_default() + app.lookup_action("notification-response").disconnect_by_func(self._notification_response) diff --git a/src/ops.py b/src/ops.py index df4adade..1726bda5 100644 --- a/src/ops.py +++ b/src/ops.py @@ -4,7 +4,7 @@ import logging from pathlib import Path -from gi.repository import GObject, GLib, Gio +from gi.repository import GObject, GLib, Gio, Gtk, Gdk import grpc @@ -283,3 +283,23 @@ def stop_transfer(self): def remove_transfer(self): self.emit("op-command", OpCommand.REMOVE_TRANSFER) +class TextMessageOp(CommonOp): + message = None + + def __init__(self, direction, sender): + super(TextMessageOp, self).__init__(direction, sender) + self.gicon = Gio.ThemedIcon.new("xsi-mail-message-new-symbolic") + self.description = _("Text message") + + def copy_message(self): + cb = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + cb.set_text(self.message, -1) + + def send_notification(self): + notifications.TextMessageNotification(self) + + def remove_transfer(self): + self.emit("op-command", OpCommand.REMOVE_TRANSFER) + + def retry_transfer(self): + self.emit("op-command", OpCommand.RETRY_TRANSFER) diff --git a/src/remote.py b/src/remote.py index 0ac1f0f7..8ec2302e 100644 --- a/src/remote.py +++ b/src/remote.py @@ -18,8 +18,8 @@ import misc import transfers import auth -from ops import SendOp, ReceiveOp -from util import TransferDirection, OpStatus, OpCommand, RemoteStatus, ReceiveError +from ops import SendOp, ReceiveOp, TextMessageOp +from util import TransferDirection, OpStatus, OpCommand, RemoteStatus, ReceiveError, RemoteFeatures _ = gettext.gettext @@ -56,6 +56,7 @@ def __init__(self, ident, hostname, display_hostname, ip_info, port, local_ident self.display_name = "" self.favorite = prefs.get_is_favorite(self.ident) self.recent_time = 0 # Keep monotonic time when visited on the user page + self.supports_messages = False self.avatar_surface = None self.transfer_ops = [] @@ -366,6 +367,8 @@ def get_info_finished(future): info = future.result() self.display_name = info.display_name self.user_name = info.user_name + feature_flags = RemoteFeatures(info.feature_flags) + self.supports_messages = RemoteFeatures.TEXT_MESSAGES in feature_flags self.favorite = prefs.get_is_favorite(self.ident) valid = GLib.utf8_make_valid(self.display_name, -1) @@ -590,6 +593,21 @@ def _send_files(uri_list): util.add_to_recents_if_single_selection(uri_list) self.rpc_call(_send_files, uri_list) + def send_text_message(self, message): + op = TextMessageOp(TransferDirection.TO_REMOTE_MACHINE, self.local_ident) + op.message = message + op.status = OpStatus.FINISHED + self.add_op(op) + self.rpc_call(self.do_send_text_message, op) + + def do_send_text_message(self, op): + try: + self.stub.SendTextMessage(warp_pb2.TextMessage(ident=self.local_ident, timestamp=op.start_time, message=op.message)) + except Exception as e: + logging.error("Sending message failed: %s" % e) + op.status = OpStatus.FAILED + op.emit_status_changed() + @misc._idle def add_op(self, op): if op not in self.transfer_ops: @@ -600,7 +618,7 @@ def add_op(self, op): if isinstance(op, SendOp): op.connect("initial-setup-complete", self.notify_remote_machine_of_new_op) self.emit("new-outgoing-op", op) - if isinstance(op, ReceiveOp): + if isinstance(op, (ReceiveOp, TextMessageOp)): self.emit("new-incoming-op", op) def set_busy(): @@ -662,8 +680,13 @@ def op_command_issued(self, op, command): elif command == OpCommand.STOP_TRANSFER_BY_SENDER: self.rpc_call(self.stop_transfer_op, op, by_sender=True) elif command == OpCommand.RETRY_TRANSFER: - op.set_status(OpStatus.WAITING_PERMISSION) - self.rpc_call(self.send_transfer_op_request, op) + if isinstance(op, TextMessageOp): + op.status = OpStatus.FINISHED + op.emit_status_changed() + self.rpc_call(self.do_send_text_message, op) + else: + op.set_status(OpStatus.WAITING_PERMISSION) + self.rpc_call(self.send_transfer_op_request, op) elif command == OpCommand.REMOVE_TRANSFER: self.remove_op(op) # receive diff --git a/src/server.py b/src/server.py index 298603db..053f4209 100644 --- a/src/server.py +++ b/src/server.py @@ -29,8 +29,8 @@ import util import misc import transfers -from ops import ReceiveOp -from util import TransferDirection, OpStatus, RemoteStatus +from ops import ReceiveOp, TextMessageOp +from util import TransferDirection, OpStatus, RemoteStatus, RemoteFeatures import zeroconf from zeroconf import ServiceInfo, Zeroconf, ServiceBrowser, IPVersion @@ -41,6 +41,8 @@ SERVICE_TYPE = "_warpinator._tcp.local." +SERVER_FEATURES = RemoteFeatures.TEXT_MESSAGES + # server (this is on a separate thread from the ui, grpc isn't compatible with # gmainloop) class Server(threading.Thread, warp_pb2_grpc.WarpServicer, GObject.Object): @@ -559,7 +561,8 @@ def GetRemoteMachineInfo(self, request, context): logging.debug("Server RPC: GetRemoteMachineInfo from '%s'" % request.readable_name) return warp_pb2.RemoteMachineInfo(display_name=GLib.get_real_name(), - user_name=GLib.get_user_name()) + user_name=GLib.get_user_name(), + feature_flags=SERVER_FEATURES) def GetRemoteMachineAvatar(self, request, context): logging.debug("Server RPC: GetRemoteMachineAvatar from '%s'" % request.readable_name) @@ -712,3 +715,20 @@ def StopTransfer(self, request, context): op.set_status(OpStatus.FAILED) return void + + def SendTextMessage(self, request, context): + logging.debug("Server RPC: SendTextMessage from '%s'" % request.ident) + try: + remote_machine:remote.RemoteMachine = self.remote_machines[request.ident] + except KeyError as e: + logging.warning("Received text message from unknown remote: %s" % e) + return + + op = TextMessageOp(TransferDirection.FROM_REMOTE_MACHINE, request.ident) + op.sender_name = remote_machine.display_name + op.message = request.message + op.status = OpStatus.FINISHED + remote_machine.add_op(op) + op.send_notification() + + return void diff --git a/src/util.py b/src/util.py index de1c6fe2..7fb73909 100644 --- a/src/util.py +++ b/src/util.py @@ -141,7 +141,7 @@ def shutdown(self, wait=True): self._factory_thread.join() logging.debug("NewThreadExecutor: Shutdown complete") -from enum import IntEnum +from enum import IntEnum, IntFlag TransferDirection = IntEnum('TransferDirection', 'TO_REMOTE_MACHINE \ FROM_REMOTE_MACHINE') @@ -192,6 +192,9 @@ def shutdown(self, wait=True): CERT_UP_TO_DATE \ FAILURE') +class RemoteFeatures(IntFlag): + TEXT_MESSAGES = 1 << 0 + class ReceiveError(Exception): def __init__(self, message, fatal=True): self.fatal = fatal diff --git a/src/warp.proto b/src/warp.proto index 88038877..9ce94333 100644 --- a/src/warp.proto +++ b/src/warp.proto @@ -18,6 +18,7 @@ service Warp { rpc GetRemoteMachineAvatar(LookupName) returns (stream RemoteMachineAvatar) {} rpc ProcessTransferOpRequest(TransferOpRequest) returns (VoidType) {} rpc PauseTransferOp(OpInfo) returns (VoidType) {} + rpc SendTextMessage(TextMessage) returns (VoidType) {} // Receiver methods rpc StartTransfer(OpInfo) returns (stream FileChunk) {} @@ -32,6 +33,7 @@ service Warp { message RemoteMachineInfo { string display_name = 1; string user_name = 2; + uint32 feature_flags = 3; } message RemoteMachineAvatar { @@ -114,3 +116,8 @@ message ServiceRegistration { string ipv6 = 7; } +message TextMessage { + string ident = 1; + uint64 timestamp = 2; + string message = 3; +} diff --git a/src/warp_pb2.py b/src/warp_pb2.py index 69f78764..6a595ef1 100644 --- a/src/warp_pb2.py +++ b/src/warp_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: warp.proto -# Protobuf Python Version: 6.31.0 +# Protobuf Python Version: 5.29.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -11,8 +11,8 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 6, - 31, + 5, + 29, 0, '', 'warp.proto' @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nwarp.proto\"<\n\x11RemoteMachineInfo\x12\x14\n\x0c\x64isplay_name\x18\x01 \x01(\t\x12\x11\n\tuser_name\x18\x02 \x01(\t\"+\n\x13RemoteMachineAvatar\x12\x14\n\x0c\x61vatar_chunk\x18\x01 \x01(\x0c\"/\n\nLookupName\x12\n\n\x02id\x18\x01 \x01(\t\x12\x15\n\rreadable_name\x18\x02 \x01(\t\"\x1e\n\nHaveDuplex\x12\x10\n\x08response\x18\x02 \x01(\x08\"\x19\n\x08VoidType\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\"Z\n\x06OpInfo\x12\r\n\x05ident\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x15\n\rreadable_name\x18\x03 \x01(\t\x12\x17\n\x0fuse_compression\x18\x04 \x01(\x08\"0\n\x08StopInfo\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\r\n\x05\x65rror\x18\x02 \x01(\x08\"\xd0\x01\n\x11TransferOpRequest\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\x13\n\x0bsender_name\x18\x02 \x01(\t\x12\x15\n\rreceiver_name\x18\x03 \x01(\t\x12\x10\n\x08receiver\x18\x04 \x01(\t\x12\x0c\n\x04size\x18\x05 \x01(\x04\x12\r\n\x05\x63ount\x18\x06 \x01(\x04\x12\x16\n\x0ename_if_single\x18\x07 \x01(\t\x12\x16\n\x0emime_if_single\x18\x08 \x01(\t\x12\x19\n\x11top_dir_basenames\x18\t \x03(\t\"\x88\x01\n\tFileChunk\x12\x15\n\rrelative_path\x18\x01 \x01(\t\x12\x11\n\tfile_type\x18\x02 \x01(\x05\x12\x16\n\x0esymlink_target\x18\x03 \x01(\t\x12\r\n\x05\x63hunk\x18\x04 \x01(\x0c\x12\x11\n\tfile_mode\x18\x05 \x01(\r\x12\x17\n\x04time\x18\x06 \x01(\x0b\x32\t.FileTime\"-\n\x08\x46ileTime\x12\r\n\x05mtime\x18\x01 \x01(\x04\x12\x12\n\nmtime_usec\x18\x02 \x01(\r\"8\n\nRegRequest\x12\n\n\x02ip\x18\x01 \x01(\t\x12\x10\n\x08hostname\x18\x02 \x01(\t\x12\x0c\n\x04ipv6\x18\x03 \x01(\t\"\"\n\x0bRegResponse\x12\x13\n\x0blocked_cert\x18\x01 \x01(\t\"\x8b\x01\n\x13ServiceRegistration\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\r\x12\x10\n\x08hostname\x18\x04 \x01(\t\x12\x13\n\x0b\x61pi_version\x18\x05 \x01(\r\x12\x11\n\tauth_port\x18\x06 \x01(\r\x12\x0c\n\x04ipv6\x18\x07 \x01(\t2\xf2\x03\n\x04Warp\x12\x33\n\x15\x43heckDuplexConnection\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12.\n\x10WaitingForDuplex\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12\x39\n\x14GetRemoteMachineInfo\x12\x0b.LookupName\x1a\x12.RemoteMachineInfo\"\x00\x12?\n\x16GetRemoteMachineAvatar\x12\x0b.LookupName\x1a\x14.RemoteMachineAvatar\"\x00\x30\x01\x12;\n\x18ProcessTransferOpRequest\x12\x12.TransferOpRequest\x1a\t.VoidType\"\x00\x12\'\n\x0fPauseTransferOp\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12(\n\rStartTransfer\x12\x07.OpInfo\x1a\n.FileChunk\"\x00\x30\x01\x12/\n\x17\x43\x61ncelTransferOpRequest\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12&\n\x0cStopTransfer\x12\t.StopInfo\x1a\t.VoidType\"\x00\x12 \n\x04Ping\x12\x0b.LookupName\x1a\t.VoidType\"\x00\x32\x86\x01\n\x10WarpRegistration\x12\x31\n\x12RequestCertificate\x12\x0b.RegRequest\x1a\x0c.RegResponse\"\x00\x12?\n\x0fRegisterService\x12\x14.ServiceRegistration\x1a\x14.ServiceRegistration\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nwarp.proto\"S\n\x11RemoteMachineInfo\x12\x14\n\x0c\x64isplay_name\x18\x01 \x01(\t\x12\x11\n\tuser_name\x18\x02 \x01(\t\x12\x15\n\rfeature_flags\x18\x03 \x01(\r\"+\n\x13RemoteMachineAvatar\x12\x14\n\x0c\x61vatar_chunk\x18\x01 \x01(\x0c\"/\n\nLookupName\x12\n\n\x02id\x18\x01 \x01(\t\x12\x15\n\rreadable_name\x18\x02 \x01(\t\"\x1e\n\nHaveDuplex\x12\x10\n\x08response\x18\x02 \x01(\x08\"\x19\n\x08VoidType\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\"Z\n\x06OpInfo\x12\r\n\x05ident\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x15\n\rreadable_name\x18\x03 \x01(\t\x12\x17\n\x0fuse_compression\x18\x04 \x01(\x08\"0\n\x08StopInfo\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\r\n\x05\x65rror\x18\x02 \x01(\x08\"\xd0\x01\n\x11TransferOpRequest\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\x13\n\x0bsender_name\x18\x02 \x01(\t\x12\x15\n\rreceiver_name\x18\x03 \x01(\t\x12\x10\n\x08receiver\x18\x04 \x01(\t\x12\x0c\n\x04size\x18\x05 \x01(\x04\x12\r\n\x05\x63ount\x18\x06 \x01(\x04\x12\x16\n\x0ename_if_single\x18\x07 \x01(\t\x12\x16\n\x0emime_if_single\x18\x08 \x01(\t\x12\x19\n\x11top_dir_basenames\x18\t \x03(\t\"\x88\x01\n\tFileChunk\x12\x15\n\rrelative_path\x18\x01 \x01(\t\x12\x11\n\tfile_type\x18\x02 \x01(\x05\x12\x16\n\x0esymlink_target\x18\x03 \x01(\t\x12\r\n\x05\x63hunk\x18\x04 \x01(\x0c\x12\x11\n\tfile_mode\x18\x05 \x01(\r\x12\x17\n\x04time\x18\x06 \x01(\x0b\x32\t.FileTime\"-\n\x08\x46ileTime\x12\r\n\x05mtime\x18\x01 \x01(\x04\x12\x12\n\nmtime_usec\x18\x02 \x01(\r\"8\n\nRegRequest\x12\n\n\x02ip\x18\x01 \x01(\t\x12\x10\n\x08hostname\x18\x02 \x01(\t\x12\x0c\n\x04ipv6\x18\x03 \x01(\t\"\"\n\x0bRegResponse\x12\x13\n\x0blocked_cert\x18\x01 \x01(\t\"\x8b\x01\n\x13ServiceRegistration\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\r\x12\x10\n\x08hostname\x18\x04 \x01(\t\x12\x13\n\x0b\x61pi_version\x18\x05 \x01(\r\x12\x11\n\tauth_port\x18\x06 \x01(\r\x12\x0c\n\x04ipv6\x18\x07 \x01(\t\"@\n\x0bTextMessage\x12\r\n\x05ident\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x0f\n\x07message\x18\x03 \x01(\t2\xa0\x04\n\x04Warp\x12\x33\n\x15\x43heckDuplexConnection\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12.\n\x10WaitingForDuplex\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12\x39\n\x14GetRemoteMachineInfo\x12\x0b.LookupName\x1a\x12.RemoteMachineInfo\"\x00\x12?\n\x16GetRemoteMachineAvatar\x12\x0b.LookupName\x1a\x14.RemoteMachineAvatar\"\x00\x30\x01\x12;\n\x18ProcessTransferOpRequest\x12\x12.TransferOpRequest\x1a\t.VoidType\"\x00\x12\'\n\x0fPauseTransferOp\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12,\n\x0fSendTextMessage\x12\x0c.TextMessage\x1a\t.VoidType\"\x00\x12(\n\rStartTransfer\x12\x07.OpInfo\x1a\n.FileChunk\"\x00\x30\x01\x12/\n\x17\x43\x61ncelTransferOpRequest\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12&\n\x0cStopTransfer\x12\t.StopInfo\x1a\t.VoidType\"\x00\x12 \n\x04Ping\x12\x0b.LookupName\x1a\t.VoidType\"\x00\x32\x86\x01\n\x10WarpRegistration\x12\x31\n\x12RequestCertificate\x12\x0b.RegRequest\x1a\x0c.RegResponse\"\x00\x12?\n\x0fRegisterService\x12\x14.ServiceRegistration\x1a\x14.ServiceRegistration\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -32,33 +32,35 @@ if not _descriptor._USE_C_DESCRIPTORS: DESCRIPTOR._loaded_options = None _globals['_REMOTEMACHINEINFO']._serialized_start=14 - _globals['_REMOTEMACHINEINFO']._serialized_end=74 - _globals['_REMOTEMACHINEAVATAR']._serialized_start=76 - _globals['_REMOTEMACHINEAVATAR']._serialized_end=119 - _globals['_LOOKUPNAME']._serialized_start=121 - _globals['_LOOKUPNAME']._serialized_end=168 - _globals['_HAVEDUPLEX']._serialized_start=170 - _globals['_HAVEDUPLEX']._serialized_end=200 - _globals['_VOIDTYPE']._serialized_start=202 - _globals['_VOIDTYPE']._serialized_end=227 - _globals['_OPINFO']._serialized_start=229 - _globals['_OPINFO']._serialized_end=319 - _globals['_STOPINFO']._serialized_start=321 - _globals['_STOPINFO']._serialized_end=369 - _globals['_TRANSFEROPREQUEST']._serialized_start=372 - _globals['_TRANSFEROPREQUEST']._serialized_end=580 - _globals['_FILECHUNK']._serialized_start=583 - _globals['_FILECHUNK']._serialized_end=719 - _globals['_FILETIME']._serialized_start=721 - _globals['_FILETIME']._serialized_end=766 - _globals['_REGREQUEST']._serialized_start=768 - _globals['_REGREQUEST']._serialized_end=824 - _globals['_REGRESPONSE']._serialized_start=826 - _globals['_REGRESPONSE']._serialized_end=860 - _globals['_SERVICEREGISTRATION']._serialized_start=863 - _globals['_SERVICEREGISTRATION']._serialized_end=1002 - _globals['_WARP']._serialized_start=1005 - _globals['_WARP']._serialized_end=1503 - _globals['_WARPREGISTRATION']._serialized_start=1506 - _globals['_WARPREGISTRATION']._serialized_end=1640 + _globals['_REMOTEMACHINEINFO']._serialized_end=97 + _globals['_REMOTEMACHINEAVATAR']._serialized_start=99 + _globals['_REMOTEMACHINEAVATAR']._serialized_end=142 + _globals['_LOOKUPNAME']._serialized_start=144 + _globals['_LOOKUPNAME']._serialized_end=191 + _globals['_HAVEDUPLEX']._serialized_start=193 + _globals['_HAVEDUPLEX']._serialized_end=223 + _globals['_VOIDTYPE']._serialized_start=225 + _globals['_VOIDTYPE']._serialized_end=250 + _globals['_OPINFO']._serialized_start=252 + _globals['_OPINFO']._serialized_end=342 + _globals['_STOPINFO']._serialized_start=344 + _globals['_STOPINFO']._serialized_end=392 + _globals['_TRANSFEROPREQUEST']._serialized_start=395 + _globals['_TRANSFEROPREQUEST']._serialized_end=603 + _globals['_FILECHUNK']._serialized_start=606 + _globals['_FILECHUNK']._serialized_end=742 + _globals['_FILETIME']._serialized_start=744 + _globals['_FILETIME']._serialized_end=789 + _globals['_REGREQUEST']._serialized_start=791 + _globals['_REGREQUEST']._serialized_end=847 + _globals['_REGRESPONSE']._serialized_start=849 + _globals['_REGRESPONSE']._serialized_end=883 + _globals['_SERVICEREGISTRATION']._serialized_start=886 + _globals['_SERVICEREGISTRATION']._serialized_end=1025 + _globals['_TEXTMESSAGE']._serialized_start=1027 + _globals['_TEXTMESSAGE']._serialized_end=1091 + _globals['_WARP']._serialized_start=1094 + _globals['_WARP']._serialized_end=1638 + _globals['_WARPREGISTRATION']._serialized_start=1641 + _globals['_WARPREGISTRATION']._serialized_end=1775 # @@protoc_insertion_point(module_scope) diff --git a/src/warp_pb2_grpc.py b/src/warp_pb2_grpc.py index 218fd5ab..0ebb8488 100644 --- a/src/warp_pb2_grpc.py +++ b/src/warp_pb2_grpc.py @@ -5,7 +5,7 @@ import warp_pb2 as warp__pb2 -GRPC_GENERATED_VERSION = '1.73.1' +GRPC_GENERATED_VERSION = '1.71.0' GRPC_VERSION = grpc.__version__ _version_not_supported = False @@ -71,6 +71,11 @@ def __init__(self, channel): request_serializer=warp__pb2.OpInfo.SerializeToString, response_deserializer=warp__pb2.VoidType.FromString, _registered_method=True) + self.SendTextMessage = channel.unary_unary( + '/Warp/SendTextMessage', + request_serializer=warp__pb2.TextMessage.SerializeToString, + response_deserializer=warp__pb2.VoidType.FromString, + _registered_method=True) self.StartTransfer = channel.unary_stream( '/Warp/StartTransfer', request_serializer=warp__pb2.OpInfo.SerializeToString, @@ -142,6 +147,12 @@ def PauseTransferOp(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def SendTextMessage(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def StartTransfer(self, request, context): """Receiver methods """ @@ -201,6 +212,11 @@ def add_WarpServicer_to_server(servicer, server): request_deserializer=warp__pb2.OpInfo.FromString, response_serializer=warp__pb2.VoidType.SerializeToString, ), + 'SendTextMessage': grpc.unary_unary_rpc_method_handler( + servicer.SendTextMessage, + request_deserializer=warp__pb2.TextMessage.FromString, + response_serializer=warp__pb2.VoidType.SerializeToString, + ), 'StartTransfer': grpc.unary_stream_rpc_method_handler( servicer.StartTransfer, request_deserializer=warp__pb2.OpInfo.FromString, @@ -401,6 +417,33 @@ def PauseTransferOp(request, metadata, _registered_method=True) + @staticmethod + def SendTextMessage(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/Warp/SendTextMessage', + warp__pb2.TextMessage.SerializeToString, + warp__pb2.VoidType.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + @staticmethod def StartTransfer(request, target, diff --git a/src/warpinator.py b/src/warpinator.py index c69de293..ae612cea 100644 --- a/src/warpinator.py +++ b/src/warpinator.py @@ -27,7 +27,7 @@ import auth import misc import networkmonitor -from ops import SendOp, ReceiveOp +from ops import SendOp, ReceiveOp, TextMessageOp from util import TransferDirection, OpStatus, RemoteStatus # XApp 2.0 required for favorites. @@ -67,7 +67,8 @@ "transfer_resume", \ "transfer_stop", \ "transfer_remove", \ - "transfer_open_folder") + "transfer_open_folder", \ + "transfer_copy_message") INIT_BUTTONS = () PERM_TO_SEND_BUTTONS = ("transfer_cancel_request",) @@ -86,6 +87,7 @@ TRANSFER_COMPLETED_SENDER_BUTTONS = TRANSFER_CANCELLED_BUTTONS TRANSFER_FILE_NOT_FOUND_BUTTONS = TRANSFER_CANCELLED_BUTTONS TRANSFER_COMPLETED_RECEIVER_BUTTONS = ("transfer_remove", "transfer_open_folder") +TRANSFER_TEXT_MESSAGE_BUTTONS = ("transfer_remove", "transfer_copy_message") class OpItem(object): def __init__(self, op): @@ -102,6 +104,7 @@ def __init__(self, op): self.op_status_stack = self.builder.get_object("op_status_stack") self.op_transfer_status_message = self.builder.get_object("op_transfer_status_message") self.op_transfer_problem_label = self.builder.get_object("op_transfer_problem_label") + self.op_transfer_text_message = self.builder.get_object("op_transfer_text_message") self.op_progress_bar = self.builder.get_object("op_transfer_progress_bar") self.accept_button = self.builder.get_object("transfer_accept") self.decline_button = self.builder.get_object("transfer_decline") @@ -111,6 +114,7 @@ def __init__(self, op): self.stop_button = self.builder.get_object("transfer_stop") self.remove_button = self.builder.get_object("transfer_remove") self.folder_button = self.builder.get_object("transfer_open_folder") + self.copy_button = self.builder.get_object("transfer_copy_message") self.accept_button.connect("clicked", self.accept_button_clicked) self.decline_button.connect("clicked", self.decline_button_clicked) @@ -120,6 +124,7 @@ def __init__(self, op): self.stop_button.connect("clicked", self.stop_button_clicked) self.remove_button.connect("clicked", self.remove_button_clicked) self.folder_button.connect("clicked", self.folder_button_clicked) + self.copy_button.connect("clicked", self.copy_button_clicked) self.op.connect("progress-changed", self.update_progress) @@ -181,7 +186,32 @@ def refresh_status_widgets(self): else: self.op_transfer_problem_label.set_text(_("Some files not found")) elif self.op.status == OpStatus.FINISHED: - self.op_transfer_status_message.set_text(_("Completed")) + if isinstance(self.op, TextMessageOp): + label_length = 80 + lines_left = 4 + lines = self.op.message.split("\n") + msg = "" + for l in lines: + wrapped_lines = math.ceil(len(l) / label_length) + if wrapped_lines > lines_left: + msg += "\n" + l[:label_length*lines_left-3] + "..." + break + else: + msg += "\n" + l + lines_left -= wrapped_lines + if lines_left < 1: + last_line_len = len(l) % label_length + if last_line_len == 0 and len(l) > 0: + last_line_len = label_length + if len(lines) > 4: + if last_line_len > label_length-3: + msg = msg[:-last_line_len+label_length-3] + msg += "..." + break + msg = msg[1:] # skip first \n + self.op_transfer_text_message.set_text(msg) + else: + self.op_transfer_status_message.set_text(_("Completed")) elif self.op.status == OpStatus.FINISHED_WARNING: self.op_transfer_status_message.set_text(_("Completed, but with errors")) @@ -197,7 +227,9 @@ def refresh_buttons_and_icons(self): self.mime_image.set_from_gicon(self.op.gicon, Gtk.IconSize.BUTTON) self.transfer_size_label.set_text(self.op.size_string) + self.transfer_size_label.set_visible(not isinstance(self.op, TextMessageOp)) self.transfer_description_label.set_text(self.op.description) + self.transfer_description_label.set_visible(not isinstance(self.op, TextMessageOp)) if self.op.status in (OpStatus.INIT, OpStatus.CALCULATING): self.op_status_stack.set_visible_child_name("calculating") @@ -234,11 +266,15 @@ def refresh_buttons_and_icons(self): self.set_visible_buttons(TRANSFER_FILE_NOT_FOUND_BUTTONS) elif self.op.status in (OpStatus.FINISHED, OpStatus.FINISHED_WARNING): - self.op_status_stack.set_visible_child_name("message") - if isinstance(self.op, SendOp): - self.set_visible_buttons(TRANSFER_COMPLETED_SENDER_BUTTONS) + if isinstance(self.op, TextMessageOp): + self.op_status_stack.set_visible_child_name("text-message") + self.set_visible_buttons(TRANSFER_TEXT_MESSAGE_BUTTONS) else: - self.set_visible_buttons(TRANSFER_COMPLETED_RECEIVER_BUTTONS) + self.op_status_stack.set_visible_child_name("message") + if isinstance(self.op, SendOp): + self.set_visible_buttons(TRANSFER_COMPLETED_SENDER_BUTTONS) + else: + self.set_visible_buttons(TRANSFER_COMPLETED_RECEIVER_BUTTONS) elif self.op.status in (OpStatus.CANCELLED_PERMISSION_BY_SENDER, OpStatus.CANCELLED_PERMISSION_BY_RECEIVER): self.set_visible_buttons(TRANSFER_CANCELLED_BUTTONS) @@ -275,6 +311,9 @@ def folder_button_clicked(self, button): util.open_save_folder(self.op.top_dir_basenames[0]) else: util.open_save_folder() + + def copy_button_clicked(self, button): + self.op.copy_message() def destroy(self): self.builder = None @@ -506,6 +545,11 @@ def __init__(self): self.user_ip_label = self.builder.get_object("user_ip") self.user_op_list = self.builder.get_object("user_op_list") self.user_send_button = self.builder.get_object("user_send_button") + self.user_send_msg_button = self.builder.get_object("user_send_msg_button") + self.user_send_msg_button.connect("clicked", self.send_msg_button_clicked) + self.user_msg_entry = self.builder.get_object("user_msg_entry") + self.user_msg_entry.connect("key-press-event", self.msg_entry_key_press) + self.user_msg_box = self.builder.get_object("user_msg_box") self.user_online_box = self.builder.get_object("user_online_box") self.user_online_image = self.builder.get_object("user_online_image") self.user_online_label = self.builder.get_object("user_online_label") @@ -633,7 +677,7 @@ def window_delete_event(self, widget, event, data=None): def window_key_press(self, widget, event, data=None): if not self.search_entry.has_focus() and self.view_stack.get_visible_child_name() == "overview": self.search_entry.grab_focus() - elif event.keyval == Gdk.KEY_BackSpace and self.view_stack.get_visible_child_name() == "user": + elif event.keyval == Gdk.KEY_BackSpace and self.view_stack.get_visible_child_name() == "user" and not self.user_msg_entry.has_focus(): self.back_to_overview() return Gdk.EVENT_STOP @@ -736,6 +780,16 @@ def recent_item_selected(self, recent_chooser, data=None): def favorite_selected(self, favorites, uri): self.current_selected_remote_machine.send_files([uri]) + def send_msg_button_clicked(self, button): + self.send_text_message() + + def msg_entry_key_press(self, entry, event, data=None): + if event.keyval == Gdk.KEY_Return and event.state & Gdk.ModifierType.CONTROL_MASK: + self.send_text_message() + return Gdk.EVENT_STOP + + return Gdk.EVENT_PROPAGATE + def open_file_picker(self, button, data=None): dialog = util.create_file_and_folder_picker(self.window) @@ -849,6 +903,12 @@ def restart_service_clicked(self, menuitem): def manual_connect_to_host(self, host): logging.debug("Connecting to " + host) + def send_text_message(self): + buf = self.user_msg_entry.get_buffer() + buf_s, buf_e = buf.get_bounds() + self.current_selected_remote_machine.send_text_message(buf.get_text(buf_s, buf_e, False)) + buf.delete(buf_s, buf_e) + def report_bad_save_folder(self): path = prefs.get_save_path() self.bad_save_folder_label.set_text(path) @@ -1043,6 +1103,8 @@ def refresh_remote_machine_view(self): else: self.user_avatar_image.set_from_icon_name("xsi-avatar-default-symbolic", Gtk.IconSize.DND) + self.user_msg_box.set_visible(remote.supports_messages) + self.add_op_items() self.sync_favorite() @@ -1053,6 +1115,7 @@ def current_selected_remote_status_changed(self, remote_machine): (entry,), Gdk.DragAction.COPY) self.user_send_button.set_sensitive(True) + self.user_send_msg_button.set_sensitive(True) self.user_online_label.set_text(_("Online")) self.user_online_image.set_from_icon_name(ICON_ONLINE, Gtk.IconSize.LARGE_TOOLBAR) self.user_online_spinner.hide() @@ -1060,6 +1123,7 @@ def current_selected_remote_status_changed(self, remote_machine): elif remote_machine.status == RemoteStatus.OFFLINE: self.user_op_list.drag_dest_unset() self.user_send_button.set_sensitive(False) + self.user_send_msg_button.set_sensitive(False) self.user_online_label.set_text(_("Offline")) self.user_online_image.set_from_icon_name(ICON_OFFLINE, Gtk.IconSize.LARGE_TOOLBAR) self.user_online_spinner.hide() @@ -1067,6 +1131,7 @@ def current_selected_remote_status_changed(self, remote_machine): elif remote_machine.status == RemoteStatus.UNREACHABLE: self.user_op_list.drag_dest_unset() self.user_send_button.set_sensitive(False) + self.user_send_msg_button.set_sensitive(False) self.user_online_label.set_text(_("Unable to connect")) self.user_online_image.set_from_icon_name(ICON_UNREACHABLE, Gtk.IconSize.LARGE_TOOLBAR) self.user_online_spinner.hide() @@ -1074,6 +1139,7 @@ def current_selected_remote_status_changed(self, remote_machine): elif remote_machine.status == RemoteStatus.AWAITING_DUPLEX: self.user_op_list.drag_dest_unset() self.user_send_button.set_sensitive(False) + self.user_send_msg_button.set_sensitive(False) self.user_online_label.set_text(_("Waiting for two-way connection")) self.user_online_image.set_from_icon_name(ICON_UNREACHABLE, Gtk.IconSize.LARGE_TOOLBAR) self.user_online_spinner.hide() @@ -1081,6 +1147,7 @@ def current_selected_remote_status_changed(self, remote_machine): else: self.user_op_list.drag_dest_unset() self.user_send_button.set_sensitive(False) + self.user_send_msg_button.set_sensitive(False) self.user_online_label.set_text(_("Connecting")) self.user_online_image.hide() self.user_online_spinner.show()