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
|
||||
|
||||
def _on_close(self):
|
||||
def hndl():
|
||||
# noinspection PyUnusedLocal
|
||||
def hndl(ws):
|
||||
self._ws = None
|
||||
self.logger.warning('Todoist websocket connection closed')
|
||||
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):
|
||||
def __init__(self, board: TrelloBoard, **kwargs):
|
||||
super().__init__(output=board, **kwargs)
|
||||
self.board = board
|
||||
|
||||
|
||||
class TrelloBoardsResponse(TrelloResponse):
|
||||
|
|
Loading…
Reference in a new issue