From 82a9aa1232a28e8206b3eab8301e1fa1f9f6ea0e Mon Sep 17 00:00:00 2001
From: Fabio Manganiello <blacklight86@gmail.com>
Date: Sat, 23 May 2020 23:11:42 +0200
Subject: [PATCH] Added clipboard backend

---
 docs/source/backends.rst                    |  1 +
 docs/source/events.rst                      |  1 +
 docs/source/platypush/backend/clipboard.rst |  5 +++
 docs/source/platypush/events/clipboard.rst  |  5 +++
 platypush/backend/clipboard.py              | 34 +++++++++++++++
 platypush/message/event/clipboard.py        |  9 ++++
 platypush/plugins/pushbullet.py             | 46 ++++++++++++++++-----
 requirements.txt                            |  3 ++
 setup.py                                    |  2 +
 9 files changed, 95 insertions(+), 11 deletions(-)
 create mode 100644 docs/source/platypush/backend/clipboard.rst
 create mode 100644 docs/source/platypush/events/clipboard.rst
 create mode 100644 platypush/backend/clipboard.py
 create mode 100644 platypush/message/event/clipboard.py

diff --git a/docs/source/backends.rst b/docs/source/backends.rst
index eedc39cd8..49cdeac9d 100644
--- a/docs/source/backends.rst
+++ b/docs/source/backends.rst
@@ -19,6 +19,7 @@ Backends
     platypush/backend/button.flic.rst
     platypush/backend/camera.pi.rst
     platypush/backend/chat.telegram.rst
+    platypush/backend/clipboard.rst
     platypush/backend/covid19.rst
     platypush/backend/foursquare.rst
     platypush/backend/google.fit.rst
diff --git a/docs/source/events.rst b/docs/source/events.rst
index e1edcb4ba..9196bacec 100644
--- a/docs/source/events.rst
+++ b/docs/source/events.rst
@@ -15,6 +15,7 @@ Events
     platypush/events/button.flic.rst
     platypush/events/camera.rst
     platypush/events/chat.telegram.rst
+    platypush/events/clipboard.rst
     platypush/events/covid19.rst
     platypush/events/distance.rst
     platypush/events/foursquare.rst
diff --git a/docs/source/platypush/backend/clipboard.rst b/docs/source/platypush/backend/clipboard.rst
new file mode 100644
index 000000000..8d6ea0a39
--- /dev/null
+++ b/docs/source/platypush/backend/clipboard.rst
@@ -0,0 +1,5 @@
+``platypush.backend.clipboard``
+===============================
+
+.. automodule:: platypush.backend.clipboard
+    :members:
diff --git a/docs/source/platypush/events/clipboard.rst b/docs/source/platypush/events/clipboard.rst
new file mode 100644
index 000000000..34ba60b6b
--- /dev/null
+++ b/docs/source/platypush/events/clipboard.rst
@@ -0,0 +1,5 @@
+``platypush.message.event.clipboard``
+=====================================
+
+.. automodule:: platypush.message.event.clipboard
+    :members:
diff --git a/platypush/backend/clipboard.py b/platypush/backend/clipboard.py
new file mode 100644
index 000000000..ece09b5d9
--- /dev/null
+++ b/platypush/backend/clipboard.py
@@ -0,0 +1,34 @@
+import time
+from typing import Optional
+
+import pyperclip
+
+from platypush.backend import Backend
+from platypush.message.event.clipboard import ClipboardEvent
+
+
+class ClipboardBackend(Backend):
+    """
+    This backend monitors for changes in the clipboard and generates even when the user copies a new text.
+
+    Requires:
+
+        - **pyperclip** (``pip install pyperclip``)
+
+    """
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self._last_text: Optional[str] = None
+
+    def run(self):
+        while not self.should_stop():
+            text = pyperclip.paste()
+            if text and text != self._last_text:
+                self.bus.post(ClipboardEvent(text=text))
+
+            self._last_text = text
+            time.sleep(0.1)
+
+
+# vim:sw=4:ts=4:et:
diff --git a/platypush/message/event/clipboard.py b/platypush/message/event/clipboard.py
new file mode 100644
index 000000000..4fc4fa748
--- /dev/null
+++ b/platypush/message/event/clipboard.py
@@ -0,0 +1,9 @@
+from platypush.message.event import Event
+
+
+class ClipboardEvent(Event):
+    def __init__(self, text: str, *args, **kwargs):
+        super().__init__(*args, text=text, **kwargs)
+
+
+# vim:sw=4:ts=4:et:
diff --git a/platypush/plugins/pushbullet.py b/platypush/plugins/pushbullet.py
index 9763e76c9..3e7ccf2a9 100644
--- a/platypush/plugins/pushbullet.py
+++ b/platypush/plugins/pushbullet.py
@@ -1,5 +1,7 @@
 import json
 import os
+from typing import Optional
+
 import requests
 
 from platypush.context import get_backend
@@ -18,12 +20,12 @@ class PushbulletPlugin(Plugin):
         * The :class:`platypush.backend.pushbullet.Pushbullet` backend enabled
     """
 
-    def __init__(self, token: str = None, *args, **kwargs):
+    def __init__(self, token: str = None, **kwargs):
         """
         :param token: Pushbullet API token. If not set the plugin will try to retrieve it from
             the Pushbullet backend configuration, if available
         """
-        super().__init__(*args, **kwargs)
+        super().__init__(**kwargs)
 
         if not token:
             backend = get_backend('pushbullet')
@@ -44,18 +46,18 @@ class PushbulletPlugin(Plugin):
         Get the list of available devices
         """
         resp = requests.get('https://api.pushbullet.com/v2/devices',
-                headers={'Authorization': 'Bearer ' + self.token,
-                    'Content-Type': 'application/json'})
+                            headers={'Authorization': 'Bearer ' + self.token,
+                                     'Content-Type': 'application/json'})
 
         self._devices = resp.json().get('devices', [])
         self._devices_by_id = {
-                dev['iden']: dev
-                for dev in self._devices
+            dev['iden']: dev
+            for dev in self._devices
         }
 
         self._devices_by_name = {
-                dev['nickname']: dev
-                for dev in self._devices if 'nickname' in dev
+            dev['nickname']: dev
+            for dev in self._devices if 'nickname' in dev
         }
 
     @action
@@ -124,7 +126,6 @@ class PushbulletPlugin(Plugin):
             if not device:
                 raise RuntimeError('No such device')
 
-        pushbullet = get_backend('pushbullet')
         resp = requests.post('https://api.pushbullet.com/v2/upload-request',
                              data=json.dumps({'file_name': os.path.basename(filename)}),
                              headers={'Authorization': 'Bearer ' + self.token,
@@ -151,7 +152,7 @@ class PushbulletPlugin(Plugin):
                                  'device_iden': device['iden'] if device else None,
                                  'file_name': r['file_name'],
                                  'file_type': r['file_type'],
-                                 'file_url': r['file_url'] }))
+                                 'file_url': r['file_url']}))
 
         if resp.status_code >= 400:
             raise Exception('Pushbullet file push failed with status {}'.
@@ -163,6 +164,29 @@ class PushbulletPlugin(Plugin):
             'url': r['file_url']
         }
 
+    @action
+    def send_clipboard(self, text: str):
+        """
+        Copy text to the clipboard of a device.
+
+        :param text: Text to be copied.
+        """
+        backend = get_backend('pushbullet')
+        device_id = backend.get_device_id() if backend else None
+
+        resp = requests.post('https://api.pushbullet.com/v2/ephemerals',
+                             data=json.dumps({
+                                 'type': 'push',
+                                 'push': {
+                                     'body': text,
+                                     'type': 'clip',
+                                     'source_device_iden': device_id,
+                                 },
+                             }),
+                             headers={'Authorization': 'Bearer ' + self.token,
+                                      'Content-Type': 'application/json'})
+
+        resp.raise_for_status()
+
 
 # vim:sw=4:ts=4:et:
-
diff --git a/requirements.txt b/requirements.txt
index 8c7a8fe6c..26bf93462 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -264,3 +264,6 @@ croniter
 
 # Support for SSH integration
 # paramiko
+
+# Support for clipboard integration
+# pyperclip
diff --git a/setup.py b/setup.py
index d874c2de5..93a526e1c 100755
--- a/setup.py
+++ b/setup.py
@@ -301,5 +301,7 @@ setup(
         'samsungtv': ['samsungtvws'],
         # Support for SSH integration
         'ssh': ['paramiko'],
+        # Support for clipboard integration
+        'clipboard': ['pyperclip'],
     },
 )