Added Trello backend: closes #90

This commit is contained in:
Fabio Manganiello 2020-01-03 16:28:49 +01:00
parent 1de3296c85
commit 8aadd5569e
4 changed files with 273 additions and 1 deletions

View file

@ -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
View 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:

View 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:

View file

@ -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):