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
+
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()