Added Trello backend: closes #90
This commit is contained in:
parent
1de3296c85
commit
8aadd5569e
4 changed files with 273 additions and 1 deletions
|
@ -65,7 +65,8 @@ class TodoistBackend(Backend):
|
||||||
return hndl
|
return hndl
|
||||||
|
|
||||||
def _on_close(self):
|
def _on_close(self):
|
||||||
def hndl():
|
# noinspection PyUnusedLocal
|
||||||
|
def hndl(ws):
|
||||||
self._ws = None
|
self._ws = None
|
||||||
self.logger.warning('Todoist websocket connection closed')
|
self.logger.warning('Todoist websocket connection closed')
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
|
195
platypush/backend/trello.py
Normal file
195
platypush/backend/trello.py
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from threading import Event
|
||||||
|
from typing import List
|
||||||
|
from websocket import WebSocketApp
|
||||||
|
|
||||||
|
from platypush.backend import Backend
|
||||||
|
from platypush.context import get_plugin
|
||||||
|
from platypush.message.event.trello import MoveCardEvent, NewCardEvent, ArchivedCardEvent, \
|
||||||
|
UnarchivedCardEvent
|
||||||
|
|
||||||
|
from platypush.plugins.trello import TrelloPlugin
|
||||||
|
|
||||||
|
|
||||||
|
class TrelloBackend(Backend):
|
||||||
|
"""
|
||||||
|
This backend listens for events on a remote Trello account through websocket interface..
|
||||||
|
Note that the Trello websocket interface
|
||||||
|
`is not officially supported <https://community.developer.atlassian.com/t/websocket-interface/34586/4>`_
|
||||||
|
and it requires a different token from the one you use for the Trello API (and for the Trello plugin).
|
||||||
|
To get the websocket token:
|
||||||
|
|
||||||
|
1. Open https://trello.com in your browser.
|
||||||
|
2. Open the developer tools (F12), go to the Network tab, select 'Websocket' or 'WS' in the filter bar
|
||||||
|
and refresh the page.
|
||||||
|
3. You should see an entry in the format ``wss://trello.com/1/Session/socket?token=<token>``.
|
||||||
|
4. Copy the ``<token>`` in the configuration of this backend.
|
||||||
|
|
||||||
|
Requires:
|
||||||
|
|
||||||
|
* **websocket-client** (``pip install websocket-client``)
|
||||||
|
* The :class:`platypush.plugins.trello.TrelloPlugin` configured.
|
||||||
|
|
||||||
|
Triggers:
|
||||||
|
|
||||||
|
* :class:`platypush.message.event.trello.NewCardEvent` when a card is created.
|
||||||
|
* :class:`platypush.message.event.MoveCardEvent` when a card is moved.
|
||||||
|
* :class:`platypush.message.event.ArchivedCardEvent` when a card is archived/closed.
|
||||||
|
* :class:`platypush.message.event.UnarchivedCardEvent` when a card is un-archived/opened.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
_websocket_url_base = 'wss://trello.com/1/Session/socket?token={token}'
|
||||||
|
|
||||||
|
def __init__(self, boards: List[str], token: str, **kwargs):
|
||||||
|
"""
|
||||||
|
:param boards: List of boards to subscribe, by ID or name.
|
||||||
|
:param token: Trello web client API token.
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._plugin: TrelloPlugin = get_plugin('trello')
|
||||||
|
self.token = token
|
||||||
|
self._req_id = 0
|
||||||
|
self._boards_by_id = {}
|
||||||
|
self._boards_by_name = {}
|
||||||
|
|
||||||
|
for b in boards:
|
||||||
|
b = self._plugin.get_board(b).board
|
||||||
|
self._boards_by_id[b.id] = b
|
||||||
|
self._boards_by_name[b.name] = b
|
||||||
|
|
||||||
|
self.url = self._websocket_url_base.format(token=self.token)
|
||||||
|
self._connected = Event()
|
||||||
|
|
||||||
|
self._items = {}
|
||||||
|
self._event_handled = False
|
||||||
|
|
||||||
|
def _initialize_connection(self, ws: WebSocketApp):
|
||||||
|
for board_id in self._boards_by_id.keys():
|
||||||
|
self._send(ws, {
|
||||||
|
'type': 'subscribe',
|
||||||
|
'modelType': 'Board',
|
||||||
|
'idModel': board_id,
|
||||||
|
'tags': ['clientActions', 'updates'],
|
||||||
|
'invitationTokens': [],
|
||||||
|
})
|
||||||
|
|
||||||
|
self.logger.info('Trello boards subscribed')
|
||||||
|
|
||||||
|
def _on_msg(self):
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
def hndl(ws: WebSocketApp, msg):
|
||||||
|
if not msg:
|
||||||
|
# Reply back with an empty message when the server sends an empty message
|
||||||
|
ws.send('')
|
||||||
|
return
|
||||||
|
|
||||||
|
# noinspection PyBroadException
|
||||||
|
try:
|
||||||
|
msg = json.loads(msg)
|
||||||
|
except:
|
||||||
|
self.logger.warning('Received invalid JSON message from Trello: {}'.format(msg))
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'error' in msg:
|
||||||
|
self.logger.warning('Trello error: {}'.format(msg['error']))
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg.get('reqid') == 0:
|
||||||
|
self.logger.debug('Ping response received, subscribing boards')
|
||||||
|
self._initialize_connection(ws)
|
||||||
|
return
|
||||||
|
|
||||||
|
notify = msg.get('notify')
|
||||||
|
if not notify:
|
||||||
|
return
|
||||||
|
|
||||||
|
if notify['event'] != 'updateModels' or notify['typeName'] != 'Action':
|
||||||
|
return
|
||||||
|
|
||||||
|
for delta in notify['deltas']:
|
||||||
|
args = {
|
||||||
|
'card_id': delta['data']['card']['id'],
|
||||||
|
'card_name': delta['data']['card']['name'],
|
||||||
|
'list_id': (delta['data'].get('list') or delta['data'].get('listAfter', {})).get('id'),
|
||||||
|
'list_name': (delta['data'].get('list') or delta['data'].get('listAfter', {})).get('name'),
|
||||||
|
'board_id': delta['data']['board']['id'],
|
||||||
|
'board_name': delta['data']['board']['name'],
|
||||||
|
'closed': delta.get('closed'),
|
||||||
|
'member_id': delta['memberCreator']['id'],
|
||||||
|
'member_username': delta['memberCreator']['username'],
|
||||||
|
'member_fullname': delta['memberCreator']['fullName'],
|
||||||
|
'date': delta['date'],
|
||||||
|
}
|
||||||
|
|
||||||
|
if delta.get('type') == 'createCard':
|
||||||
|
self.bus.post(NewCardEvent(**args))
|
||||||
|
elif delta.get('type') == 'updateCard':
|
||||||
|
if 'listBefore' in delta['data']:
|
||||||
|
args.update({
|
||||||
|
'old_list_id': delta['data']['listBefore']['id'],
|
||||||
|
'old_list_name': delta['data']['listBefore']['name'],
|
||||||
|
})
|
||||||
|
|
||||||
|
self.bus.post(MoveCardEvent(**args))
|
||||||
|
elif 'closed' in delta['data'].get('old', {}):
|
||||||
|
cls = UnarchivedCardEvent if delta['data']['old']['closed'] else ArchivedCardEvent
|
||||||
|
self.bus.post(cls(**args))
|
||||||
|
|
||||||
|
return hndl
|
||||||
|
|
||||||
|
def _on_error(self):
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
def hndl(ws: WebSocketApp, error):
|
||||||
|
self.logger.warning('Trello websocket error: {}'.format(error))
|
||||||
|
|
||||||
|
return hndl
|
||||||
|
|
||||||
|
def _on_close(self):
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
def hndl(ws: WebSocketApp):
|
||||||
|
self.logger.warning('Trello websocket connection closed')
|
||||||
|
self._connected.clear()
|
||||||
|
self._req_id = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
self._connect()
|
||||||
|
self._connected.wait(timeout=20)
|
||||||
|
break
|
||||||
|
except TimeoutError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return hndl
|
||||||
|
|
||||||
|
def _on_open(self):
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
|
def hndl(ws: WebSocketApp):
|
||||||
|
self._connected.set()
|
||||||
|
self._send(ws, {'type': 'ping'})
|
||||||
|
self.logger.info('Trello websocket connected')
|
||||||
|
|
||||||
|
return hndl
|
||||||
|
|
||||||
|
def _send(self, ws: WebSocketApp, msg: dict):
|
||||||
|
msg['reqid'] = self._req_id
|
||||||
|
ws.send(json.dumps(msg))
|
||||||
|
self._req_id += 1
|
||||||
|
|
||||||
|
def _connect(self) -> WebSocketApp:
|
||||||
|
return WebSocketApp(self.url,
|
||||||
|
on_open=self._on_open(),
|
||||||
|
on_message=self._on_msg(),
|
||||||
|
on_error=self._on_error(),
|
||||||
|
on_close=self._on_close())
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
super().run()
|
||||||
|
self.logger.info('Started Todoist backend')
|
||||||
|
ws = self._connect()
|
||||||
|
ws.run_forever()
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
75
platypush/message/event/trello.py
Normal file
75
platypush/message/event/trello.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from platypush.message.event import Event
|
||||||
|
|
||||||
|
|
||||||
|
class TrelloEvent(Event):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CardEvent(TrelloEvent):
|
||||||
|
def __init__(self,
|
||||||
|
card_id: str,
|
||||||
|
card_name: str,
|
||||||
|
list_id: str,
|
||||||
|
list_name: str,
|
||||||
|
board_id: str,
|
||||||
|
board_name: str,
|
||||||
|
closed: bool,
|
||||||
|
member_id: str,
|
||||||
|
member_username: str,
|
||||||
|
member_fullname: str,
|
||||||
|
date: datetime.datetime,
|
||||||
|
*args, **kwargs):
|
||||||
|
super().__init__(*args,
|
||||||
|
card_id=card_id,
|
||||||
|
card_name=card_name,
|
||||||
|
list_id=list_id,
|
||||||
|
list_name=list_name,
|
||||||
|
board_id=board_id,
|
||||||
|
board_name=board_name,
|
||||||
|
closed=closed,
|
||||||
|
member_id=member_id,
|
||||||
|
member_username=member_username,
|
||||||
|
member_fullname=member_fullname,
|
||||||
|
date=date,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class NewCardEvent(CardEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when a card is created.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MoveCardEvent(CardEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when a card is moved to another list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, old_list_id: str, old_list_name: str, *args, **kwargs):
|
||||||
|
super().__init__(*args, old_list_id=old_list_id, old_list_name=old_list_name, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ArchivedCardEvent(CardEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when a card is archived.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['old_closed'] = False
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class UnarchivedCardEvent(CardEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when a card is un-archived.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['old_closed'] = True
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
|
@ -37,6 +37,7 @@ class TrelloBoard(Mapping):
|
||||||
class TrelloBoardResponse(TrelloResponse):
|
class TrelloBoardResponse(TrelloResponse):
|
||||||
def __init__(self, board: TrelloBoard, **kwargs):
|
def __init__(self, board: TrelloBoard, **kwargs):
|
||||||
super().__init__(output=board, **kwargs)
|
super().__init__(output=board, **kwargs)
|
||||||
|
self.board = board
|
||||||
|
|
||||||
|
|
||||||
class TrelloBoardsResponse(TrelloResponse):
|
class TrelloBoardsResponse(TrelloResponse):
|
||||||
|
|
Loading…
Reference in a new issue