forked from platypush/platypush
[#302] Merged pushbullet
backend and plugin.
Also, added support for more granular Pushbullet events. Closes: #302
This commit is contained in:
parent
4e1943d197
commit
9fa5989e21
13 changed files with 905 additions and 374 deletions
|
@ -419,9 +419,7 @@ backend](https://docs.platypush.tech/en/latest/platypush/backend/http.html), an
|
||||||
[MQTT
|
[MQTT
|
||||||
instance](https://docs.platypush.tech/en/latest/platypush/backend/mqtt.html), a
|
instance](https://docs.platypush.tech/en/latest/platypush/backend/mqtt.html), a
|
||||||
[Kafka
|
[Kafka
|
||||||
instance](https://docs.platypush.tech/en/latest/platypush/backend/kafka.html),
|
instance](https://docs.platypush.tech/en/latest/platypush/backend/kafka.html).
|
||||||
[Pushbullet](https://docs.platypush.tech/en/latest/platypush/backend/pushbullet.html)
|
|
||||||
etc.).
|
|
||||||
|
|
||||||
If a backend supports the execution of requests (e.g. HTTP, MQTT, Kafka,
|
If a backend supports the execution of requests (e.g. HTTP, MQTT, Kafka,
|
||||||
Websocket and TCP) then you can send requests to these services in JSON format.
|
Websocket and TCP) then you can send requests to these services in JSON format.
|
||||||
|
|
|
@ -23,7 +23,6 @@ Backends
|
||||||
platypush/backend/nextcloud.rst
|
platypush/backend/nextcloud.rst
|
||||||
platypush/backend/nfc.rst
|
platypush/backend/nfc.rst
|
||||||
platypush/backend/nodered.rst
|
platypush/backend/nodered.rst
|
||||||
platypush/backend/pushbullet.rst
|
|
||||||
platypush/backend/redis.rst
|
platypush/backend/redis.rst
|
||||||
platypush/backend/scard.rst
|
platypush/backend/scard.rst
|
||||||
platypush/backend/sensor.ir.zeroborg.rst
|
platypush/backend/sensor.ir.zeroborg.rst
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
``pushbullet``
|
|
||||||
================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.pushbullet
|
|
||||||
:members:
|
|
||||||
|
|
|
@ -6,26 +6,27 @@
|
||||||
import Utils from "@/Utils";
|
import Utils from "@/Utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Pushbullet",
|
|
||||||
mixins: [Utils],
|
mixins: [Utils],
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onMessage(event) {
|
onMessage(event) {
|
||||||
if (event.push_type === 'mirror') {
|
this.notify({
|
||||||
this.notify({
|
title: event.title,
|
||||||
title: event.title,
|
text: event.body,
|
||||||
text: event.body,
|
image: {
|
||||||
image: {
|
src: event.icon ? 'data:image/png;base64, ' + event.icon : undefined,
|
||||||
src: event.icon ? 'data:image/png;base64, ' + event.icon : undefined,
|
icon: event.icon ? undefined : 'bell',
|
||||||
icon: event.icon ? undefined : 'bell',
|
},
|
||||||
},
|
})
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.subscribe(this.onMessage, null, 'platypush.message.event.pushbullet.PushbulletEvent')
|
this.subscribe(
|
||||||
|
this.onMessage,
|
||||||
|
null,
|
||||||
|
'platypush.message.event.pushbullet.PushbulletNotificationEvent'
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,204 +0,0 @@
|
||||||
import json
|
|
||||||
import time
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from platypush.backend import Backend
|
|
||||||
from platypush.message.event.pushbullet import PushbulletEvent
|
|
||||||
|
|
||||||
|
|
||||||
class PushbulletBackend(Backend):
|
|
||||||
"""
|
|
||||||
This backend will listen for events on a Pushbullet (https://pushbullet.com)
|
|
||||||
channel and propagate them to the bus. This backend is quite useful if you
|
|
||||||
want to synchronize events and actions with your mobile phone (through the
|
|
||||||
Pushbullet app and/or through Tasker), synchronize clipboards, send pictures
|
|
||||||
and files to other devices etc. You can also wrap Platypush messages as JSON
|
|
||||||
into a push body to execute them.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
token: str,
|
|
||||||
device: str = 'Platypush',
|
|
||||||
proxy_host: Optional[str] = None,
|
|
||||||
proxy_port: Optional[int] = None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
:param token: Your Pushbullet API token, see https://docs.pushbullet.com/#authentication
|
|
||||||
:param device: Name of the virtual device for Platypush (default: Platypush)
|
|
||||||
:param proxy_host: HTTP proxy host (default: None)
|
|
||||||
:param proxy_port: HTTP proxy port (default: None)
|
|
||||||
"""
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
self.token = token
|
|
||||||
self.device_name = device
|
|
||||||
self.proxy_host = proxy_host
|
|
||||||
self.proxy_port = proxy_port
|
|
||||||
self.device = None
|
|
||||||
self.pb_device_id = None
|
|
||||||
self.pb = None
|
|
||||||
self.listener = None
|
|
||||||
|
|
||||||
def _initialize(self):
|
|
||||||
# noinspection PyPackageRequirements
|
|
||||||
from pushbullet import Pushbullet
|
|
||||||
|
|
||||||
self.pb = Pushbullet(self.token)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.device = self.pb.get_device(self.device_name)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.info(
|
|
||||||
f'Device {self.device_name} does not exist: {e}. Creating it'
|
|
||||||
)
|
|
||||||
self.device = self.pb.new_device(self.device_name)
|
|
||||||
|
|
||||||
self.pb_device_id = self.get_device_id()
|
|
||||||
|
|
||||||
def _get_latest_push(self):
|
|
||||||
t = int(time.time()) - 5
|
|
||||||
pushes = self.pb.get_pushes(modified_after=str(t), limit=1)
|
|
||||||
if pushes:
|
|
||||||
return pushes[0]
|
|
||||||
|
|
||||||
def on_push(self):
|
|
||||||
def callback(data):
|
|
||||||
try:
|
|
||||||
# Parse the push
|
|
||||||
try:
|
|
||||||
data = json.loads(data) if isinstance(data, str) else data
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.exception(e)
|
|
||||||
return
|
|
||||||
|
|
||||||
# If it's a push, get it
|
|
||||||
if data['type'] == 'tickle' and data['subtype'] == 'push':
|
|
||||||
push = self._get_latest_push()
|
|
||||||
elif data['type'] == 'push':
|
|
||||||
push = data['push']
|
|
||||||
else:
|
|
||||||
return # Not a push notification
|
|
||||||
|
|
||||||
if not push:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Post an event, useful to react on mobile notifications if
|
|
||||||
# you enabled notification mirroring on your PushBullet app
|
|
||||||
event = PushbulletEvent(**push)
|
|
||||||
self.on_message(event)
|
|
||||||
|
|
||||||
if 'body' not in push:
|
|
||||||
return
|
|
||||||
self.logger.debug(f'Received push: {push}')
|
|
||||||
|
|
||||||
body = push['body']
|
|
||||||
try:
|
|
||||||
body = json.loads(body)
|
|
||||||
self.on_message(body)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.debug(
|
|
||||||
'Unexpected message received on the '
|
|
||||||
+ f'Pushbullet backend: {e}. Message: {body}'
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.exception(e)
|
|
||||||
return
|
|
||||||
|
|
||||||
return callback
|
|
||||||
|
|
||||||
def get_device_id(self):
|
|
||||||
# noinspection PyBroadException
|
|
||||||
try:
|
|
||||||
return self.pb.get_device(self.device_name).device_iden
|
|
||||||
except Exception:
|
|
||||||
device = self.pb.new_device(
|
|
||||||
self.device_name,
|
|
||||||
model='Platypush virtual device',
|
|
||||||
manufacturer='platypush',
|
|
||||||
icon='system',
|
|
||||||
)
|
|
||||||
|
|
||||||
self.logger.info(f'Created Pushbullet device {self.device_name}')
|
|
||||||
return device.device_iden
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if self.listener:
|
|
||||||
self.listener.close()
|
|
||||||
self.listener = None
|
|
||||||
|
|
||||||
def on_stop(self):
|
|
||||||
self.logger.info('Received STOP event on the Pushbullet backend')
|
|
||||||
super().on_stop()
|
|
||||||
self.close()
|
|
||||||
self.logger.info('Pushbullet backend terminated')
|
|
||||||
|
|
||||||
def on_close(self, err=None):
|
|
||||||
def callback(*_):
|
|
||||||
self.listener = None
|
|
||||||
raise RuntimeError(err or 'Connection closed')
|
|
||||||
|
|
||||||
return callback
|
|
||||||
|
|
||||||
def on_error(self, *_):
|
|
||||||
def callback(*args):
|
|
||||||
self.logger.error(f'Pushbullet error: {args}')
|
|
||||||
try:
|
|
||||||
if self.listener:
|
|
||||||
self.listener.close()
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error('Error on Pushbullet connection close upon error')
|
|
||||||
self.logger.exception(e)
|
|
||||||
finally:
|
|
||||||
self.listener = None
|
|
||||||
|
|
||||||
return callback
|
|
||||||
|
|
||||||
def on_open(self):
|
|
||||||
def callback(*_):
|
|
||||||
self.logger.info('Pushbullet service connected')
|
|
||||||
|
|
||||||
return callback
|
|
||||||
|
|
||||||
def run_listener(self):
|
|
||||||
from .listener import Listener
|
|
||||||
|
|
||||||
self.logger.info(
|
|
||||||
f'Initializing Pushbullet backend - device_id: {self.device_name}'
|
|
||||||
)
|
|
||||||
self.listener = Listener(
|
|
||||||
account=self.pb,
|
|
||||||
on_push=self.on_push(),
|
|
||||||
on_open=self.on_open(),
|
|
||||||
on_close=self.on_close(),
|
|
||||||
on_error=self.on_error(),
|
|
||||||
http_proxy_host=self.proxy_host,
|
|
||||||
http_proxy_port=self.proxy_port,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.listener.run_forever()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
super().run()
|
|
||||||
initialized = False
|
|
||||||
|
|
||||||
while not initialized:
|
|
||||||
try:
|
|
||||||
self._initialize()
|
|
||||||
initialized = True
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.exception(e)
|
|
||||||
self.logger.error(f'Pushbullet initialization error: {e}')
|
|
||||||
time.sleep(10)
|
|
||||||
|
|
||||||
while not self.should_stop():
|
|
||||||
try:
|
|
||||||
self.run_listener()
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.exception(e)
|
|
||||||
time.sleep(10)
|
|
||||||
self.logger.info('Retrying connection')
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -1,16 +0,0 @@
|
||||||
manifest:
|
|
||||||
events:
|
|
||||||
platypush.message.event.pushbullet.PushbulletEvent: if a new push is received
|
|
||||||
apk:
|
|
||||||
- git
|
|
||||||
apt:
|
|
||||||
- git
|
|
||||||
dnf:
|
|
||||||
- git
|
|
||||||
pacman:
|
|
||||||
- git
|
|
||||||
install:
|
|
||||||
pip:
|
|
||||||
- git+https://github.com/rbrcsk/pushbullet.py
|
|
||||||
package: platypush.backend.pushbullet
|
|
||||||
type: backend
|
|
|
@ -74,7 +74,7 @@ def register_backends(bus=None, global_scope=False, **kwargs):
|
||||||
for name, cfg in Config.get_backends().items():
|
for name, cfg in Config.get_backends().items():
|
||||||
module = importlib.import_module('platypush.backend.' + name)
|
module = importlib.import_module('platypush.backend.' + name)
|
||||||
|
|
||||||
# e.g. backend.pushbullet main class: PushbulletBackend
|
# e.g. backend.http main class: HttpBackend
|
||||||
cls_name = ''
|
cls_name = ''
|
||||||
for token in module.__name__.title().split('.')[2:]:
|
for token in module.__name__.title().split('.')[2:]:
|
||||||
cls_name += token.title()
|
cls_name += token.title()
|
||||||
|
|
|
@ -1,30 +1,184 @@
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
from platypush.message.event import Event
|
from platypush.message.event import Event
|
||||||
|
|
||||||
|
|
||||||
class PushbulletEvent(Event):
|
class PushbulletEvent(Event):
|
||||||
"""
|
"""
|
||||||
PushBullet event object.
|
Base PushBullet event.
|
||||||
|
|
||||||
If you have configured the PushBullet backend with your account token,
|
|
||||||
and enabled notification mirroring on the PushBullet app on your mobile
|
|
||||||
devices, then the backend will trigger a PushbulletEvent whenever
|
|
||||||
a new notiification hits your mobile, and you can react to that event
|
|
||||||
through hooks that can, for example, log your notifications on a database,
|
|
||||||
display them on a dashboard, let the built-in text-to-speech plugin read
|
|
||||||
them out loud to you if they match the package name of your news app,
|
|
||||||
display them on your smart watch if they are pictures, and so on.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(
|
||||||
""" Platypush supports by default the PushBullet notification mirror
|
self,
|
||||||
format, https://docs.pushbullet.com/#mirrored-notifications """
|
*args,
|
||||||
|
notification_id: str,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
body: Optional[Union[str, dict, list]] = None,
|
||||||
|
url: Optional[str] = None,
|
||||||
|
source_device: Optional[str] = None,
|
||||||
|
source_user: Optional[str] = None,
|
||||||
|
target_device: Optional[str] = None,
|
||||||
|
icon: Optional[str] = None,
|
||||||
|
created: float,
|
||||||
|
modified: float,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param notification_id: Notification ID.
|
||||||
|
:param title: Notification title.
|
||||||
|
:param body: Notification body.
|
||||||
|
:param url: Notification URL.
|
||||||
|
:param source_device: Source device ID.
|
||||||
|
:param source_user: Source user ID.
|
||||||
|
:param target_device: Target device ID.
|
||||||
|
:param icon: Notification icon.
|
||||||
|
:param created: Notification creation timestamp.
|
||||||
|
:param modified: Notification modification timestamp.
|
||||||
|
:param kwargs: Additional attributes.
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
*args,
|
||||||
|
notification_id=notification_id,
|
||||||
|
title=title,
|
||||||
|
body=body,
|
||||||
|
url=url,
|
||||||
|
source_device=source_device,
|
||||||
|
source_user=source_user,
|
||||||
|
target_device=target_device,
|
||||||
|
icon=icon,
|
||||||
|
created=created,
|
||||||
|
modified=modified,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
if 'type' in kwargs:
|
|
||||||
# Prevent name clash with event type attribute
|
|
||||||
kwargs['push_type'] = kwargs.pop('type')
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
class PushbulletMessageEvent(PushbulletEvent):
|
||||||
|
"""
|
||||||
|
Triggered when a new message is received.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*args,
|
||||||
|
sender_id: Optional[str] = None,
|
||||||
|
sender_email: Optional[str] = None,
|
||||||
|
sender_name: Optional[str] = None,
|
||||||
|
receiver_id: Optional[str] = None,
|
||||||
|
receiver_email: Optional[str] = None,
|
||||||
|
receiver_name: Optional[str] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
*args,
|
||||||
|
sender_id=sender_id,
|
||||||
|
sender_email=sender_email,
|
||||||
|
sender_name=sender_name,
|
||||||
|
receiver_id=receiver_id,
|
||||||
|
receiver_email=receiver_email,
|
||||||
|
receiver_name=receiver_name,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PushbulletNotificationEvent(PushbulletEvent):
|
||||||
|
"""
|
||||||
|
Triggered when a notification is mirrored from another device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*args,
|
||||||
|
title: str,
|
||||||
|
body: str,
|
||||||
|
dismissible: bool,
|
||||||
|
application_name: Optional[str] = None,
|
||||||
|
package_name: Optional[str] = None,
|
||||||
|
actions: Optional[dict] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param title: Mirror notification title.
|
||||||
|
:param body: Mirror notification body.
|
||||||
|
:param dismissible: True if the notification can be dismissed.
|
||||||
|
:param application_name: Application name.
|
||||||
|
:param package_name: Package name.
|
||||||
|
:param actions: Actions associated to the notification. Example:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"label": "previous",
|
||||||
|
"trigger_key": "com.termux.api_0_6107998_previous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "pause",
|
||||||
|
"trigger_key": "com.termux.api_0_6107998_pause"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "play",
|
||||||
|
"trigger_key": "com.termux.api_0_6107998_play"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "next",
|
||||||
|
"trigger_key": "com.termux.api_0_6107998_next"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
*args,
|
||||||
|
title=title,
|
||||||
|
body=body,
|
||||||
|
dismissible=dismissible,
|
||||||
|
application_name=application_name,
|
||||||
|
package_name=package_name,
|
||||||
|
actions=actions,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PushbulletDismissalEvent(PushbulletEvent):
|
||||||
|
"""
|
||||||
|
Triggered when a notification is dismissed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, package_name: Optional[str] = None, **kwargs):
|
||||||
|
super().__init__(*args, package_name=package_name, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class PushbulletLinkEvent(PushbulletMessageEvent):
|
||||||
|
"""
|
||||||
|
Triggered when a push with a link is received.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PushbulletFileEvent(PushbulletMessageEvent):
|
||||||
|
"""
|
||||||
|
Triggered when a push with a file is received.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*args,
|
||||||
|
file_name: str,
|
||||||
|
file_type: str,
|
||||||
|
file_url: str,
|
||||||
|
image_width: Optional[int] = None,
|
||||||
|
image_height: Optional[int] = None,
|
||||||
|
image_url: Optional[str] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
*args,
|
||||||
|
file_name=file_name,
|
||||||
|
file_type=file_type,
|
||||||
|
file_url=file_url,
|
||||||
|
image_width=image_width,
|
||||||
|
image_height=image_height,
|
||||||
|
image_url=image_url,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
|
@ -1,70 +1,277 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import Optional
|
import time
|
||||||
|
from enum import Enum
|
||||||
|
from threading import Event, RLock
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from platypush.context import get_backend
|
from platypush.config import Config
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.message.event.pushbullet import (
|
||||||
|
PushbulletDismissalEvent,
|
||||||
|
PushbulletEvent,
|
||||||
|
PushbulletFileEvent,
|
||||||
|
PushbulletLinkEvent,
|
||||||
|
PushbulletMessageEvent,
|
||||||
|
PushbulletNotificationEvent,
|
||||||
|
)
|
||||||
|
from platypush.plugins import RunnablePlugin, action
|
||||||
|
from platypush.schemas.pushbullet import PushbulletDeviceSchema, PushbulletSchema
|
||||||
|
|
||||||
|
|
||||||
class PushbulletPlugin(Plugin):
|
class PushbulletType(Enum):
|
||||||
"""
|
"""
|
||||||
This plugin allows you to send pushes and files to your PushBullet account.
|
PushBullet event types.
|
||||||
Note: This plugin will only work if the :mod:`platypush.backend.pushbullet`
|
|
||||||
backend is configured.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
|
|
||||||
* The :class:`platypush.backend.pushbullet.PushbulletBackend` backend enabled
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, token: Optional[str] = None, **kwargs):
|
DISMISSAL = 'dismissal'
|
||||||
|
FILE = 'file'
|
||||||
|
LINK = 'link'
|
||||||
|
MESSAGE = 'message'
|
||||||
|
MIRROR = 'mirror'
|
||||||
|
NOTE = 'note'
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PushbulletEventType:
|
||||||
|
"""
|
||||||
|
PushBullet event type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
type: PushbulletType
|
||||||
|
event_class: Type[PushbulletEvent]
|
||||||
|
|
||||||
|
|
||||||
|
_push_event_types = [
|
||||||
|
PushbulletEventType(
|
||||||
|
type=PushbulletType.DISMISSAL,
|
||||||
|
event_class=PushbulletDismissalEvent,
|
||||||
|
),
|
||||||
|
PushbulletEventType(
|
||||||
|
type=PushbulletType.FILE,
|
||||||
|
event_class=PushbulletFileEvent,
|
||||||
|
),
|
||||||
|
PushbulletEventType(
|
||||||
|
type=PushbulletType.LINK,
|
||||||
|
event_class=PushbulletLinkEvent,
|
||||||
|
),
|
||||||
|
PushbulletEventType(
|
||||||
|
type=PushbulletType.MESSAGE,
|
||||||
|
event_class=PushbulletMessageEvent,
|
||||||
|
),
|
||||||
|
PushbulletEventType(
|
||||||
|
type=PushbulletType.MIRROR,
|
||||||
|
event_class=PushbulletNotificationEvent,
|
||||||
|
),
|
||||||
|
PushbulletEventType(
|
||||||
|
type=PushbulletType.NOTE,
|
||||||
|
event_class=PushbulletMessageEvent,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
_push_events_by_type = {t.type.value: t for t in _push_event_types}
|
||||||
|
|
||||||
|
|
||||||
|
class PushbulletPlugin(RunnablePlugin):
|
||||||
|
"""
|
||||||
|
`PushBullet <https://www.pushbullet.com/>`_ integration.
|
||||||
|
|
||||||
|
Among the other things, this plugin allows you to easily interact with your
|
||||||
|
mobile devices that have the app installed from Platypush.
|
||||||
|
|
||||||
|
If notification mirroring is enabled on your device, then the push
|
||||||
|
notifications will be mirrored to Platypush as well as PushBullet events.
|
||||||
|
|
||||||
|
Since PushBullet also comes with a Tasker integration, you can also use this
|
||||||
|
plugin to send commands to your Android device and trigger actions on it.
|
||||||
|
It can be used to programmatically send files to your devices and manage
|
||||||
|
shared clipboards too.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_timeout = 15.0
|
||||||
|
_upload_timeout = 600.0
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
token: str,
|
||||||
|
device: Optional[str] = None,
|
||||||
|
enable_mirroring: bool = True,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param token: Pushbullet API token. If not set the plugin will try to retrieve it from
|
:param token: PushBullet API token, see https://docs.pushbullet.com/#authentication.
|
||||||
the Pushbullet backend configuration, if available
|
:param device: Device ID that should be exposed. Default: ``Platypush @
|
||||||
|
<device_id | hostname>``.
|
||||||
|
:param enable_mirroring: If set to True (default) then the plugin will
|
||||||
|
receive notifications mirrored from other connected devices -
|
||||||
|
these will also be rendered on the connected web clients. Disable
|
||||||
|
it if you don't want to forward your mobile notifications through
|
||||||
|
the plugin.
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
if not token:
|
if not device:
|
||||||
backend = get_backend('pushbullet')
|
device = f'Platypush @ {Config.get_device_id()}'
|
||||||
if not backend or not backend.token:
|
|
||||||
raise AttributeError('No Pushbullet token specified')
|
|
||||||
|
|
||||||
self.token = backend.token
|
|
||||||
else:
|
|
||||||
self.token = token
|
|
||||||
|
|
||||||
|
self.token = token
|
||||||
|
self.device_name = device
|
||||||
|
self.enable_mirroring = enable_mirroring
|
||||||
|
self.listener = None
|
||||||
|
self._initialized = Event()
|
||||||
|
self._device = None
|
||||||
|
self._init_lock = RLock()
|
||||||
|
self._pb = None
|
||||||
|
self._device_id = None
|
||||||
self._devices = []
|
self._devices = []
|
||||||
self._devices_by_id = {}
|
self._devices_by_id = {}
|
||||||
self._devices_by_name = {}
|
self._devices_by_name = {}
|
||||||
|
|
||||||
@action
|
def _initialize(self):
|
||||||
def get_devices(self):
|
from pushbullet import Pushbullet
|
||||||
|
|
||||||
|
if self._initialized.is_set():
|
||||||
|
return
|
||||||
|
|
||||||
|
self._pb = Pushbullet(self.token)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._device = self._pb.get_device(self.device_name)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.info(
|
||||||
|
'Device %s does not exist: %s. Creating it',
|
||||||
|
self.device_name,
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
self._device = self._pb.new_device(self.device_name)
|
||||||
|
|
||||||
|
self._device_id = self.get_device_id()
|
||||||
|
self._initialized.set()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pb(self):
|
||||||
"""
|
"""
|
||||||
Get the list of available devices
|
:return: PushBullet API object.
|
||||||
"""
|
"""
|
||||||
resp = requests.get(
|
with self._init_lock:
|
||||||
'https://api.pushbullet.com/v2/devices',
|
self._initialize()
|
||||||
|
|
||||||
|
assert self._pb
|
||||||
|
return self._pb
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self):
|
||||||
|
"""
|
||||||
|
:return: Current PushBullet device object.
|
||||||
|
"""
|
||||||
|
with self._init_lock:
|
||||||
|
self._initialize()
|
||||||
|
|
||||||
|
assert self._device
|
||||||
|
return self._device
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_id(self):
|
||||||
|
return self.device.device_iden
|
||||||
|
|
||||||
|
def _request(self, method: str, url: str, **kwargs):
|
||||||
|
meth = getattr(requests, method)
|
||||||
|
resp = meth(
|
||||||
|
'https://api.pushbullet.com/v2/' + url.lstrip('/'),
|
||||||
|
timeout=self._timeout,
|
||||||
headers={
|
headers={
|
||||||
'Authorization': 'Bearer ' + self.token,
|
'Authorization': 'Bearer ' + self.token,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._devices = resp.json().get('devices', [])
|
resp.raise_for_status()
|
||||||
self._devices_by_id = {dev['iden']: dev for dev in self._devices}
|
return resp.json()
|
||||||
|
|
||||||
self._devices_by_name = {
|
def get_device_id(self):
|
||||||
dev['nickname']: dev for dev in self._devices if 'nickname' in dev
|
assert self._pb
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
try:
|
||||||
def get_device(self, device) -> Optional[dict]:
|
return self._pb.get_device(self.device_name).device_iden
|
||||||
"""
|
except Exception:
|
||||||
:param device: Device ID or name
|
device = self.pb.new_device(
|
||||||
"""
|
self.device_name,
|
||||||
|
model='Platypush virtual device',
|
||||||
|
manufacturer='platypush',
|
||||||
|
icon='system',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logger.info('Created Pushbullet device %s', self.device_name)
|
||||||
|
return device.device_iden
|
||||||
|
|
||||||
|
def _get_latest_push(self):
|
||||||
|
t = int(time.time()) - 10
|
||||||
|
pushes = self.pb.get_pushes(modified_after=str(t), limit=1)
|
||||||
|
if pushes:
|
||||||
|
return pushes[0]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def on_open(self, *_, **__):
|
||||||
|
self.logger.info('Pushbullet connected')
|
||||||
|
|
||||||
|
def on_close(self, args=None):
|
||||||
|
err = args[0] if args else None
|
||||||
|
self.close()
|
||||||
|
assert not err or self.should_stop(), 'Pushbullet connection closed: ' + str(
|
||||||
|
err or 'unknown error'
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_error(self, *args):
|
||||||
|
raise RuntimeError('Pushbullet error: ' + str(args))
|
||||||
|
|
||||||
|
def on_push(self, data):
|
||||||
|
try:
|
||||||
|
# Parse the push
|
||||||
|
try:
|
||||||
|
data = json.loads(data) if isinstance(data, str) else data
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.exception(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If it's a push, get it
|
||||||
|
push = None
|
||||||
|
if data['type'] == 'tickle' and data['subtype'] == 'push':
|
||||||
|
push = self._get_latest_push()
|
||||||
|
elif data['type'] == 'push':
|
||||||
|
push = data['push']
|
||||||
|
|
||||||
|
if not push:
|
||||||
|
self.logger.debug('Not a push notification.\nMessage: %s', data)
|
||||||
|
return
|
||||||
|
|
||||||
|
push_type = push.pop('type', None)
|
||||||
|
push_event_type = _push_events_by_type.get(push_type)
|
||||||
|
if not push_event_type:
|
||||||
|
self.logger.debug(
|
||||||
|
'Unknown push type: %s.\nMessage: %s', push_type, data
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
not self.enable_mirroring
|
||||||
|
and push_event_type.type == PushbulletType.MIRROR
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
push = dict(PushbulletSchema().dump(push))
|
||||||
|
evt_type = push_event_type.event_class
|
||||||
|
self._bus.post(evt_type(**push))
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'Error while processing push: %s.\nMessage: %s', e, data
|
||||||
|
)
|
||||||
|
self.logger.exception(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _get_device(self, device) -> Optional[dict]:
|
||||||
output = None
|
output = None
|
||||||
refreshed = False
|
refreshed = False
|
||||||
|
|
||||||
|
@ -79,6 +286,78 @@ class PushbulletPlugin(Plugin):
|
||||||
self.get_devices()
|
self.get_devices()
|
||||||
refreshed = True
|
refreshed = True
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.listener:
|
||||||
|
try:
|
||||||
|
self.listener.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.listener = None
|
||||||
|
|
||||||
|
if self._pb:
|
||||||
|
self._pb = None
|
||||||
|
|
||||||
|
self._initialized.clear()
|
||||||
|
|
||||||
|
def run_listener(self):
|
||||||
|
from .listener import Listener
|
||||||
|
|
||||||
|
self.listener = Listener(
|
||||||
|
account=self.pb,
|
||||||
|
on_push=self.on_push,
|
||||||
|
on_open=self.on_open,
|
||||||
|
on_close=self.on_close,
|
||||||
|
on_error=self.on_error,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.listener.run_forever()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_devices(self):
|
||||||
|
"""
|
||||||
|
Get the list of available devices.
|
||||||
|
|
||||||
|
:return: .. schema:: pushbullet.PushbulletDeviceSchema(many=True)
|
||||||
|
"""
|
||||||
|
resp = self._request('get', 'devices')
|
||||||
|
self._devices = resp.get('devices', [])
|
||||||
|
self._devices_by_id = {dev['iden']: dev for dev in self._devices}
|
||||||
|
self._devices_by_name = {
|
||||||
|
dev['nickname']: dev for dev in self._devices if 'nickname' in dev
|
||||||
|
}
|
||||||
|
|
||||||
|
return PushbulletDeviceSchema(many=True).dump(self._devices)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_device(self, device: str) -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
Get a device by ID or name.
|
||||||
|
|
||||||
|
:param device: Device ID or name
|
||||||
|
:return: .. schema:: pushbullet.PushbulletDeviceSchema
|
||||||
|
"""
|
||||||
|
dev = self._get_device(device)
|
||||||
|
if not dev:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return dict(PushbulletDeviceSchema().dump(dev))
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_pushes(self, limit: int = 10):
|
||||||
|
"""
|
||||||
|
Get the list of pushes.
|
||||||
|
|
||||||
|
:param limit: Maximum number of pushes to fetch (default: 10).
|
||||||
|
:return: .. schema:: pushbullet.PushbulletSchema(many=True)
|
||||||
|
"""
|
||||||
|
return PushbulletSchema().dump(
|
||||||
|
self._request('get', 'pushes', params={'limit': limit}).get('pushes', []),
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_note(
|
def send_note(
|
||||||
self,
|
self,
|
||||||
|
@ -96,13 +375,13 @@ class PushbulletPlugin(Plugin):
|
||||||
:param title: Note title
|
:param title: Note title
|
||||||
:param url: URL attached to the note
|
:param url: URL attached to the note
|
||||||
:param kwargs: Push arguments, see https://docs.pushbullet.com/#create-push
|
:param kwargs: Push arguments, see https://docs.pushbullet.com/#create-push
|
||||||
|
:return: .. schema:: pushbullet.PushbulletSchema
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dev = None
|
dev = None
|
||||||
if device:
|
if device:
|
||||||
dev = self.get_device(device).output
|
dev = self._get_device(device)
|
||||||
if not dev:
|
assert dev, f'No such device: {device}'
|
||||||
raise RuntimeError(f'No such device: {device}')
|
|
||||||
|
|
||||||
kwargs['body'] = body
|
kwargs['body'] = body
|
||||||
kwargs['title'] = title
|
kwargs['title'] = title
|
||||||
|
@ -114,19 +393,8 @@ class PushbulletPlugin(Plugin):
|
||||||
if dev:
|
if dev:
|
||||||
kwargs['device_iden'] = dev['iden']
|
kwargs['device_iden'] = dev['iden']
|
||||||
|
|
||||||
resp = requests.post(
|
rs = self._request('post', 'pushes', data=json.dumps(kwargs))
|
||||||
'https://api.pushbullet.com/v2/pushes',
|
return dict(PushbulletSchema().dump(rs))
|
||||||
data=json.dumps(kwargs),
|
|
||||||
headers={
|
|
||||||
'Authorization': 'Bearer ' + self.token,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if resp.status_code >= 400:
|
|
||||||
raise Exception(
|
|
||||||
f'Pushbullet push failed with status {resp.status_code}: {resp.json()}'
|
|
||||||
)
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_file(self, filename: str, device: Optional[str] = None):
|
def send_file(self, filename: str, device: Optional[str] = None):
|
||||||
|
@ -139,90 +407,85 @@ class PushbulletPlugin(Plugin):
|
||||||
|
|
||||||
dev = None
|
dev = None
|
||||||
if device:
|
if device:
|
||||||
dev = self.get_device(device).output
|
dev = self._get_device(device)
|
||||||
if not dev:
|
assert dev, f'No such device: {device}'
|
||||||
raise RuntimeError(f'No such device: {device}')
|
|
||||||
|
|
||||||
resp = requests.post(
|
upload_req = self._request(
|
||||||
'https://api.pushbullet.com/v2/upload-request',
|
'post',
|
||||||
|
'upload-request',
|
||||||
data=json.dumps({'file_name': os.path.basename(filename)}),
|
data=json.dumps({'file_name': os.path.basename(filename)}),
|
||||||
headers={
|
|
||||||
'Authorization': 'Bearer ' + self.token,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if resp.status_code != 200:
|
|
||||||
raise Exception(
|
|
||||||
f'Pushbullet file upload request failed with status {resp.status_code}'
|
|
||||||
)
|
|
||||||
|
|
||||||
r = resp.json()
|
|
||||||
with open(filename, 'rb') as f:
|
with open(filename, 'rb') as f:
|
||||||
resp = requests.post(r['upload_url'], data=r['data'], files={'file': f})
|
rs = requests.post(
|
||||||
|
upload_req['upload_url'],
|
||||||
if resp.status_code != 204:
|
data=upload_req['data'],
|
||||||
raise Exception(
|
files={'file': f},
|
||||||
f'Pushbullet file upload failed with status {resp.status_code}'
|
timeout=self._upload_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
resp = requests.post(
|
rs.raise_for_status()
|
||||||
'https://api.pushbullet.com/v2/pushes',
|
self._request(
|
||||||
headers={
|
'post',
|
||||||
'Authorization': 'Bearer ' + self.token,
|
'pushes',
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
data=json.dumps(
|
data=json.dumps(
|
||||||
{
|
{
|
||||||
'type': 'file',
|
'type': 'file',
|
||||||
'device_iden': dev['iden'] if dev else None,
|
'device_iden': dev['iden'] if dev else None,
|
||||||
'file_name': r['file_name'],
|
'file_name': upload_req['file_name'],
|
||||||
'file_type': r['file_type'],
|
'file_type': upload_req.get('file_type'),
|
||||||
'file_url': r['file_url'],
|
'file_url': upload_req['file_url'],
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if resp.status_code >= 400:
|
|
||||||
raise Exception(
|
|
||||||
f'Pushbullet file push failed with status {resp.status_code}'
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'filename': r['file_name'],
|
'filename': upload_req['file_name'],
|
||||||
'type': r['file_type'],
|
'type': upload_req.get('file_type'),
|
||||||
'url': r['file_url'],
|
'url': upload_req['file_url'],
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def send_clipboard(self, text: str):
|
def send_clipboard(self, text: str):
|
||||||
"""
|
"""
|
||||||
Copy text to the clipboard of a device.
|
Send text to the clipboard of other devices.
|
||||||
|
|
||||||
:param text: Text to be copied.
|
:param text: Text to be copied.
|
||||||
"""
|
"""
|
||||||
backend = get_backend('pushbullet')
|
self._request(
|
||||||
device_id = backend.get_device_id() if backend else None
|
'post',
|
||||||
|
'ephemerals',
|
||||||
resp = requests.post(
|
|
||||||
'https://api.pushbullet.com/v2/ephemerals',
|
|
||||||
data=json.dumps(
|
data=json.dumps(
|
||||||
{
|
{
|
||||||
'type': 'push',
|
'type': 'push',
|
||||||
'push': {
|
'push': {
|
||||||
'body': text,
|
'body': text,
|
||||||
'type': 'clip',
|
'type': 'clip',
|
||||||
'source_device_iden': device_id,
|
'source_device_iden': self.device_id,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
headers={
|
|
||||||
'Authorization': 'Bearer ' + self.token,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
resp.raise_for_status()
|
def main(self):
|
||||||
|
while not self.should_stop():
|
||||||
|
while not self._initialized.is_set():
|
||||||
|
try:
|
||||||
|
self._initialize()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.exception(e)
|
||||||
|
self.logger.error('Pushbullet initialization error: %s', e)
|
||||||
|
self.wait_stop(10)
|
||||||
|
|
||||||
|
while not self.should_stop():
|
||||||
|
try:
|
||||||
|
self.run_listener()
|
||||||
|
except Exception as e:
|
||||||
|
if not self.should_stop():
|
||||||
|
self.logger.exception(e)
|
||||||
|
self.logger.error('Pushbullet listener error: %s', e)
|
||||||
|
|
||||||
|
self.wait_stop(10)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -9,11 +9,14 @@ class Listener(_Listener):
|
||||||
"""
|
"""
|
||||||
Extends the Pushbullet Listener object by adding ``on_open`` and ``on_close`` handlers.
|
Extends the Pushbullet Listener object by adding ``on_open`` and ``on_close`` handlers.
|
||||||
"""
|
"""
|
||||||
def __init__(self,
|
|
||||||
*args,
|
def __init__(
|
||||||
on_open: Optional[Callable[[], None]] = None,
|
self,
|
||||||
on_close: Optional[Callable[[], None]] = None,
|
*args,
|
||||||
**kwargs):
|
on_open: Optional[Callable[[], None]] = None,
|
||||||
|
on_close: Optional[Callable[[], None]] = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._on_open_hndl = on_open
|
self._on_open_hndl = on_open
|
||||||
self._on_close_hndl = on_close
|
self._on_close_hndl = on_close
|
||||||
|
@ -35,7 +38,7 @@ class Listener(_Listener):
|
||||||
try:
|
try:
|
||||||
self._on_close_hndl()
|
self._on_close_hndl()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f'Pushbullet listener close error: {e}')
|
self.logger.warning('Pushbullet listener close error: %s', e)
|
||||||
|
|
||||||
return callback
|
return callback
|
||||||
|
|
|
@ -1,6 +1,21 @@
|
||||||
manifest:
|
manifest:
|
||||||
events: {}
|
|
||||||
install:
|
|
||||||
pip: []
|
|
||||||
package: platypush.plugins.pushbullet
|
package: platypush.plugins.pushbullet
|
||||||
type: plugin
|
type: plugin
|
||||||
|
events:
|
||||||
|
- platypush.message.event.pushbullet.PushbulletDismissalEvent
|
||||||
|
- platypush.message.event.pushbullet.PushbulletFileEvent
|
||||||
|
- platypush.message.event.pushbullet.PushbulletLinkEvent
|
||||||
|
- platypush.message.event.pushbullet.PushbulletMessageEvent
|
||||||
|
- platypush.message.event.pushbullet.PushbulletMessageEvent
|
||||||
|
- platypush.message.event.pushbullet.PushbulletNotificationEvent
|
||||||
|
install:
|
||||||
|
apk:
|
||||||
|
- git
|
||||||
|
apt:
|
||||||
|
- git
|
||||||
|
dnf:
|
||||||
|
- git
|
||||||
|
pacman:
|
||||||
|
- git
|
||||||
|
pip:
|
||||||
|
- git+https://github.com/rbrcsk/pushbullet.py
|
||||||
|
|
324
platypush/schemas/pushbullet.py
Normal file
324
platypush/schemas/pushbullet.py
Normal file
|
@ -0,0 +1,324 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from marshmallow import EXCLUDE, fields, pre_dump
|
||||||
|
from marshmallow.schema import Schema
|
||||||
|
|
||||||
|
from platypush.schemas import DateTime
|
||||||
|
|
||||||
|
|
||||||
|
class PushbulletActionSchema(Schema):
|
||||||
|
"""
|
||||||
|
Schema for Pushbullet notification actions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
label = fields.String(
|
||||||
|
required=True,
|
||||||
|
metadata={
|
||||||
|
'description': 'Label of the action',
|
||||||
|
'example': 'Example action',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
trigger_key = fields.String(
|
||||||
|
required=True,
|
||||||
|
metadata={
|
||||||
|
'description': 'Key of the action',
|
||||||
|
'example': 'example_action',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PushbulletSchema(Schema):
|
||||||
|
"""
|
||||||
|
Schema for Pushbullet API messages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
class Meta: # type: ignore
|
||||||
|
"""
|
||||||
|
Exclude unknown fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
unknown = EXCLUDE
|
||||||
|
|
||||||
|
notification_id = fields.String(
|
||||||
|
required=True,
|
||||||
|
metadata={
|
||||||
|
'description': 'Unique identifier for the notification/message',
|
||||||
|
'example': '12345',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
title = fields.String(
|
||||||
|
metadata={
|
||||||
|
'description': 'Title of the notification/message',
|
||||||
|
'example': 'Hello world',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
body = fields.Raw(
|
||||||
|
metadata={
|
||||||
|
'description': 'Body of the notification/message',
|
||||||
|
'example': 'Example body',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
url = fields.Url(
|
||||||
|
metadata={
|
||||||
|
'description': 'URL attached to the notification/message',
|
||||||
|
'example': 'https://example.com',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
source_user = fields.String(
|
||||||
|
attribute='source_user_iden',
|
||||||
|
metadata={
|
||||||
|
'description': 'Source user of the notification/message',
|
||||||
|
'example': 'user123',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
source_device = fields.String(
|
||||||
|
attribute='source_device_iden',
|
||||||
|
metadata={
|
||||||
|
'description': 'Source device of the notification/message',
|
||||||
|
'example': 'device123',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
target_device = fields.String(
|
||||||
|
attribute='target_device_iden',
|
||||||
|
metadata={
|
||||||
|
'description': 'Target device of the notification/message',
|
||||||
|
'example': 'device456',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
sender_id = fields.String(
|
||||||
|
attribute='sender_iden',
|
||||||
|
metadata={
|
||||||
|
'description': 'Sender ID of the notification/message',
|
||||||
|
'example': '12345',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
sender_email = fields.Email(
|
||||||
|
attribute='sender_email_normalized',
|
||||||
|
metadata={
|
||||||
|
'description': 'Sender email of the notification/message',
|
||||||
|
'example': 'user1@example.com',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
sender_name = fields.String(
|
||||||
|
metadata={
|
||||||
|
'description': 'Sender name of the notification/message',
|
||||||
|
'example': 'John Doe',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
receiver_id = fields.String(
|
||||||
|
attribute='receiver_iden',
|
||||||
|
metadata={
|
||||||
|
'description': 'Receiver ID of the notification/message',
|
||||||
|
'example': '12346',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
receiver_email = fields.Email(
|
||||||
|
attribute='receiver_email_normalized',
|
||||||
|
metadata={
|
||||||
|
'description': 'Receiver email of the notification/message',
|
||||||
|
'example': 'user2@example.com',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
dismissible = fields.Boolean(
|
||||||
|
metadata={
|
||||||
|
'description': 'Whether the notification/message is dismissible',
|
||||||
|
'example': True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
icon = fields.String(
|
||||||
|
metadata={
|
||||||
|
'description': 'Base64 encoded icon of the notification/message',
|
||||||
|
'example': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABpUlEQVQ4T',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
application_name = fields.String(
|
||||||
|
metadata={
|
||||||
|
'description': 'Name of the application that sent the notification/message',
|
||||||
|
'example': 'Example app',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
package_name = fields.String(
|
||||||
|
metadata={
|
||||||
|
'description': 'Package name of the application that sent the notification/message',
|
||||||
|
'example': 'com.example.app',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
file_name = fields.String(
|
||||||
|
metadata={
|
||||||
|
'description': 'Name of the file attached to the notification/message',
|
||||||
|
'example': 'example.txt',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
file_type = fields.String(
|
||||||
|
metadata={
|
||||||
|
'description': 'Type of the file attached to the notification/message',
|
||||||
|
'example': 'text/plain',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
file_url = fields.Url(
|
||||||
|
metadata={
|
||||||
|
'description': 'URL of the file attached to the notification/message',
|
||||||
|
'example': 'https://example.com/example.txt',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
image_width = fields.Integer(
|
||||||
|
metadata={
|
||||||
|
'description': 'Width of the image attached to the notification/message',
|
||||||
|
'example': 100,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
image_height = fields.Integer(
|
||||||
|
metadata={
|
||||||
|
'description': 'Height of the image attached to the notification/message',
|
||||||
|
'example': 100,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
image_url = fields.Url(
|
||||||
|
metadata={
|
||||||
|
'description': 'URL of the image attached to the notification/message',
|
||||||
|
'example': 'https://example.com/example.png',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
actions = fields.Nested(
|
||||||
|
PushbulletActionSchema,
|
||||||
|
many=True,
|
||||||
|
metadata={
|
||||||
|
'description': 'Actions of the notification/message',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
created = DateTime(
|
||||||
|
metadata={
|
||||||
|
'description': 'Creation timestamp of the notification/message',
|
||||||
|
'example': '2021-01-01T00:00:00',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
modified = DateTime(
|
||||||
|
metadata={
|
||||||
|
'description': 'Last modification timestamp of the notification/message',
|
||||||
|
'example': '2021-01-01T00:00:00',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@pre_dump
|
||||||
|
def pre_dump(self, data, **_):
|
||||||
|
"""
|
||||||
|
Pre-dump hook.
|
||||||
|
"""
|
||||||
|
|
||||||
|
data['notification_id'] = str(
|
||||||
|
data.pop('iden', data.pop('notification_id', None))
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.get('body') is not None:
|
||||||
|
try:
|
||||||
|
data['body'] = json.loads(data['body'])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class PushbulletDeviceSchema(Schema):
|
||||||
|
"""
|
||||||
|
Schema for Pushbullet devices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
active = fields.Boolean(
|
||||||
|
metadata={
|
||||||
|
'description': 'Whether the device is active',
|
||||||
|
'example': True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
device_id = fields.String(
|
||||||
|
required=True,
|
||||||
|
attribute='iden',
|
||||||
|
metadata={
|
||||||
|
'description': 'Unique identifier for the device',
|
||||||
|
'example': '12345',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
name = fields.String(
|
||||||
|
attribute='nickname',
|
||||||
|
metadata={
|
||||||
|
'description': 'Name of the device',
|
||||||
|
'example': 'Example device',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
kind = fields.String(
|
||||||
|
metadata={
|
||||||
|
'description': 'Kind of the device',
|
||||||
|
'example': 'android',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
manufacturer = fields.String(
|
||||||
|
metadata={
|
||||||
|
'description': 'Manufacturer of the device',
|
||||||
|
'example': 'Example manufacturer',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
model = fields.String(
|
||||||
|
metadata={
|
||||||
|
'description': 'Model of the device',
|
||||||
|
'example': 'Example model',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
icon = fields.String(
|
||||||
|
metadata={
|
||||||
|
'description': 'Device icon type',
|
||||||
|
'example': 'system',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
pushable = fields.Boolean(
|
||||||
|
metadata={
|
||||||
|
'description': 'Whether it is possible to push notifications and '
|
||||||
|
'messages to the device',
|
||||||
|
'example': True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
created = DateTime(
|
||||||
|
metadata={
|
||||||
|
'description': 'Creation timestamp of the device',
|
||||||
|
'example': '2021-01-01T00:00:00',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
modified = DateTime(
|
||||||
|
metadata={
|
||||||
|
'description': 'Last modification timestamp of the device',
|
||||||
|
'example': '2021-01-01T00:00:00',
|
||||||
|
},
|
||||||
|
)
|
2
setup.py
2
setup.py
|
@ -91,7 +91,7 @@ setup(
|
||||||
extras_require={
|
extras_require={
|
||||||
# Support for Kafka backend and plugin
|
# Support for Kafka backend and plugin
|
||||||
'kafka': ['kafka-python'],
|
'kafka': ['kafka-python'],
|
||||||
# Support for Pushbullet backend and plugin
|
# Support for Pushbullet
|
||||||
'pushbullet': [
|
'pushbullet': [
|
||||||
'pushbullet.py @ https://github.com/rbrcsk/pushbullet.py/tarball/master'
|
'pushbullet.py @ https://github.com/rbrcsk/pushbullet.py/tarball/master'
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in a new issue