platypush/platypush/plugins/trello/__init__.py

1168 lines
35 KiB
Python

import datetime
import json
from threading import Event
from typing import Optional, Dict, List, Union
import trello
from trello.board import Board, List as List_ # type: ignore
from trello.exceptions import ResourceUnavailable # type: ignore
from websocket import WebSocketApp
from platypush.context import get_bus
from platypush.message.event.trello import (
MoveCardEvent,
NewCardEvent,
ArchivedCardEvent,
UnarchivedCardEvent,
)
from platypush.plugins import RunnablePlugin, action
from platypush.schemas.trello import (
TrelloBoardSchema,
TrelloCardSchema,
TrelloListSchema,
TrelloMemberSchema,
)
from ._model import (
TrelloBoard,
TrelloCard,
TrelloAttachment,
TrelloPreview,
TrelloChecklist,
TrelloChecklistItem,
TrelloUser,
TrelloComment,
TrelloLabel,
TrelloList,
TrelloMember,
)
class TrelloPlugin(RunnablePlugin):
"""
Trello integration.
You'll need a Trello API key. You can get it `here <https://trello.com/app-key>`.
You'll also need an auth token if you want to view/change private
resources. You can generate a permanent token linked to your account on
``https://trello.com/1/connect?key=<KEY>&name=platypush&response_type=token&expiration=never&scope=read,write``.
Also, polling of events requires you to follow a separate procedure to
retrieve the Websocket tokens, since Trello uses a different (and
undocumented) authorization mechanism:
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?clientVersion=...&x-b3-traceid=...&x-b3-spanid=...``.
4. Copy the ``x-b3-traceid`` and ``x-b3-spanid`` values into the
configuration of this plugin.
5. Go to the Cookies tab
6. Copy the value of the ``cloud.session.token`` cookie.
"""
_websocket_url_base = 'wss://trello.com/1/Session/socket?clientVersion=build-194674'
def __init__(
self,
api_key: str,
api_secret: Optional[str] = None,
token: Optional[str] = None,
cloud_session_token: Optional[str] = None,
boards: Optional[List[str]] = None,
**kwargs,
):
"""
:param api_key: Trello API key. You can get it `here <https://trello.com/app-key>`.
:param api_secret: Trello API secret. You can get it `here <https://trello.com/app-key>`.
:param token: Trello token. It is required if you want to access or modify private resources. You can get
a permanent token on
``https://trello.com/1/connect?key=<KEY>&name=platypush&response_type=token&expiration=never&scope=read,write``.
:param cloud_session_token: Cloud session token. It is required
if you want to monitor your boards for changes. See
:class:`platypush.plugins.trello.TrelloPlugin` for the procedure to
retrieve it.
:param boards: List of boards to subscribe, by ID or name. If
specified, then the plugin will listen for changes only on these boards.
"""
super().__init__(**kwargs)
self.api_key = api_key
self.api_secret = api_secret
self.token = token
self.cloud_session_token = cloud_session_token
self._client = None
self._req_id = 0
self._boards_by_id = {}
self._boards_by_name = {}
self._monitored_boards = boards
self.url = None
if token:
self.url = self._websocket_url_base
self._connected = Event()
self._items = {}
self._event_handled = False
self._ws: Optional[WebSocketApp] = None
def _get_client(self) -> trello.TrelloClient: # type: ignore
if not self._client:
self._client = trello.TrelloClient( # type: ignore
api_key=self.api_key,
api_secret=self.api_secret,
token=self.token,
)
return self._client
def _get_board(self, board: str) -> Board:
client = self._get_client()
try:
return client.get_board(board)
except ResourceUnavailable:
boards = [b for b in client.list_boards() if b.name == board]
assert boards, f'No such board: {board}'
return boards[0]
def _get_boards(
self, all: bool = False # pylint: disable=redefined-builtin
) -> List[Board]:
client = self._get_client()
return client.list_boards(board_filter='all' if all else 'open')
@action
def get_boards(self, all: bool = False): # pylint: disable=redefined-builtin
"""
Get the list of boards.
:param all: If True, return all the boards included those that have
been closed/archived/deleted. Otherwise, only return open/active
boards (default: False).
"""
return TrelloBoardSchema().dump(
[
TrelloBoard(
id=b.id,
name=b.name,
description=b.description,
url=b.url,
date_last_activity=b.date_last_activity,
closed=b.closed,
)
for b in self._get_boards(all=all)
],
many=True,
)
@action
def get_board(self, board: str):
"""
Get the info about a board.
:param board: Board ID or name
"""
b = self._get_board(board)
return TrelloBoardSchema().dump(
TrelloBoard(
id=b.id,
name=b.name,
url=b.url,
closed=b.closed,
description=b.description,
date_last_activity=b.date_last_activity,
lists=[
TrelloList(
id=ll.id,
name=ll.name,
closed=ll.closed,
subscribed=ll.subscribed,
)
for ll in b.list_lists()
],
)
)
@action
def open_board(self, board: str):
"""
Re-open/un-archive a board.
:param board: Board ID or name
"""
self._get_board(board).open()
@action
def close_board(self, board: str):
"""
Close/archive a board.
:param board: Board ID or name
"""
self._get_board(board).close()
@action
def set_board_name(self, board: str, name: str):
"""
Change the name of a board.
:param board: Board ID or name.
:param name: New name.
"""
self._get_board(board).set_name(name)
return self.get_board(name)
@action
def set_board_description(self, board: str, description: str):
"""
Change the description of a board.
:param board: Board ID or name.
:param description: New description.
"""
self._get_board(board).set_description(description)
return self.get_board(description)
@action
def create_label(self, board: str, name: str, color: Optional[str] = None):
"""
Add a label to a board.
:param board: Board ID or name
:param name: Label name
:param color: Optional HTML color
"""
self._get_board(board).add_label(name=name, color=color)
@action
def delete_label(self, board: str, label: str):
"""
Delete a label from a board.
:param board: Board ID or name
:param label: Label ID or name
"""
b = self._get_board(board)
try:
b.delete_label(label)
except ResourceUnavailable:
label_ = next(iter(ll for ll in b.get_labels() if ll.name == label), None)
assert label_, f'No such label: {label}'
label = label_.id
b.delete_label(label)
@action
def add_member(self, board: str, member_id: str, member_type: str = 'normal'):
"""
Add a member to a board.
:param board: Board ID or name.
:param member_id: Member ID to add.
:param member_type: Member type - can be 'normal' or 'admin' (default: 'normal').
"""
self._get_board(board).add_member(member_id, member_type=member_type)
@action
def remove_member(self, board: str, member_id: str):
"""
Remove a member from a board.
:param board: Board ID or name.
:param member_id: Member ID to remove.
"""
self._get_board(board).remove_member(member_id)
def _get_members(self, board: str, only_admin: bool = False):
b = self._get_board(board)
members = b.admin_members() if only_admin else b.get_members()
return TrelloMemberSchema().dump(
[
TrelloMember(
id=m.id,
fullname=m.full_name,
bio=m.bio,
url=m.url,
username=m.username,
initials=m.initials,
member_type=getattr(m, 'member_type', None),
)
for m in members
],
many=True,
)
@action
def get_members(self, board: str):
"""
Get the list of all the members of a board.
:param board: Board ID or name.
"""
return self._get_members(board, only_admin=False)
@action
def get_admin_members(self, board: str):
"""
Get the list of the admin members of a board.
:param board: Board ID or name.
"""
return self._get_members(board, only_admin=True)
@action
def get_lists(
self, board: str, all: bool = False # pylint: disable=redefined-builtin
):
"""
Get the list of lists on a board.
:param board: Board ID or name
:param all: If True, return all the lists, included those that have been closed/archived/deleted. Otherwise,
only return open/active lists (default: False).
"""
return TrelloListSchema().dump(
[
TrelloList(
id=ll.id, name=ll.name, closed=ll.closed, subscribed=ll.subscribed
)
for ll in self._get_board(board).list_lists('all' if all else 'open')
],
many=True,
)
@action
def add_list(self, board: str, name: str, pos: Optional[int] = None):
"""
Add a list to a board.
:param board: Board ID or name
:param name: List name
:param pos: Optional position (default: last)
"""
self._get_board(board).add_list(name=name, pos=pos)
def _get_list(
self, board: str, list: str # pylint: disable=redefined-builtin
) -> List_:
b = self._get_board(board)
try:
return b.get_list(list)
except ResourceUnavailable:
lists = [ll for ll in b.list_lists() if ll.name == list]
assert lists, f'No such list: {list}'
return lists[0]
@action
def set_list_name(
self, board: str, list: str, name: str # pylint: disable=redefined-builtin
):
"""
Change the name of a board list.
:param board: Board ID or name
:param list: List ID or name
:param name: New name
"""
self._get_list(board, list).set_name(name)
@action
def list_subscribe(
self, board: str, list: str # pylint: disable=redefined-builtin
):
"""
Subscribe to a list.
:param board: Board ID or name
:param list: List ID or name
"""
self._get_list(board, list).subscribe()
@action
def list_unsubscribe(
self, board: str, list: str # pylint: disable=redefined-builtin
):
"""
Unsubscribe from a list.
:param board: Board ID or name
:param list: List ID or name
"""
self._get_list(board, list).unsubscribe()
@action
def archive_all_cards(
self, board: str, list: str # pylint: disable=redefined-builtin
):
"""
Archive all the cards on a list.
:param board: Board ID or name
:param list: List ID or name
"""
self._get_list(board, list).archive_all_cards()
@action
def move_all_cards(self, board: str, src: str, dest: str):
"""
Move all the cards from a list to another.
:param board: Board ID or name
:param src: Source list
:param dest: Target list
"""
src_list = self._get_list(board, src)
dest_list = self._get_list(board, dest)
src_list.move_all_cards(dest_list.id)
@action
def open_list(self, board: str, list: str): # pylint: disable=redefined-builtin
"""
Open/un-archive a list.
:param board: Board ID or name
:param list: List ID or name
"""
self._get_list(board, list).open()
@action
def close_list(self, board: str, list: str): # pylint: disable=redefined-builtin
"""
Close/archive a list.
:param board: Board ID or name
:param list: List ID or name
"""
self._get_list(board, list).close()
@action
def move_list(
self, board: str, list: str, position: int # pylint: disable=redefined-builtin
):
"""
Move a list to another position.
:param board: Board ID or name
:param list: List ID or name
:param position: New position index
"""
self._get_list(board, list).move(position)
@action
def add_card(
self,
board: str,
list: str, # pylint: disable=redefined-builtin
name: str,
description: Optional[str] = None,
position: Optional[int] = None,
labels: Optional[List[str]] = None,
due: Optional[Union[str, datetime.datetime]] = None,
source: Optional[str] = None,
assign: Optional[List[str]] = None,
):
"""
Add a card to a list.
:param board: Board ID or name
:param list: List ID or name
:param name: Card name
:param description: Card description
:param position: Card position index
:param labels: List of labels
:param due: Due date (``datetime.datetime`` object or ISO-format string)
:param source: Card ID to clone from
:param assign: List of assignee member IDs
"""
list_ = self._get_list(board, list)
if labels:
labels = [
ll
for ll in list_.board.get_labels()
if ll.id in labels or ll.name in labels
]
card = list_.add_card(
name=name,
desc=description,
labels=labels,
due=due,
source=source,
position=position,
assign=assign,
)
return TrelloCardSchema().dump(
TrelloCard(
id=card.id,
name=card.name,
url=card.url,
closed=card.closed,
board=TrelloBoard(
id=list_.board.id,
name=list_.board.name,
url=list_.board.url,
closed=list_.board.closed,
description=list_.board.description,
date_last_activity=list_.board.date_last_activity,
),
is_due_complete=card.is_due_complete,
list=None,
comments=[],
labels=[
TrelloLabel(id=lb.id, name=lb.name, color=lb.color)
for lb in (card.labels or [])
],
description=card.description,
due_date=card.due_date,
latest_card_move_date=card.latestCardMove_date,
date_last_activity=card.date_last_activity,
)
)
@action
def delete_card(self, card_id: str):
"""
Permanently delete a card.
:param card_id: Card ID
"""
client = self._get_client()
card = client.get_card(card_id)
card.delete()
@action
def open_card(self, card_id: str):
"""
Open/un-archive a card.
:param card_id: Card ID
"""
client = self._get_client()
card = client.get_card(card_id)
card.set_closed(False)
@action
def close_card(self, card_id: str):
"""
Close/archive a card.
:param card_id: Card ID
"""
client = self._get_client()
card = client.get_card(card_id)
card.set_closed(True)
@action
def add_checklist(
self,
card_id: str,
title: str,
items: List[str],
states: Optional[List[bool]] = None,
):
"""
Add a checklist to a card.
:param card_id: Card ID
:param title: Checklist title
:param items: List of items in the checklist
:param states: State of each item, True for checked, False for unchecked
"""
client = self._get_client()
card = client.get_card(card_id)
card.add_checklist(title, items, states)
@action
def add_label(self, card_id: str, label: str):
"""
Add a label to a card.
:param card_id: Card ID
:param label: Label name
"""
client = self._get_client()
card = client.get_card(card_id)
labels = [ll for ll in card.board.get_labels() if ll.name == label]
assert labels, f'No such label: {label}'
label = labels[0]
card.add_label(label)
@action
def remove_label(self, card_id: str, label: str):
"""
Remove a label from a card.
:param card_id: Card ID
:param label: Label name
"""
client = self._get_client()
card = client.get_card(card_id)
labels = [ll for ll in card.board.get_labels() if ll.name == label]
assert labels, f'No such label: {label}'
label = labels[0]
card.remove_label(label)
@action
def assign_card(self, card_id: str, member_id: str):
"""
Assign a card.
:param card_id: Card ID
:param member_id: Member ID
"""
client = self._get_client()
card = client.get_card(card_id)
card.assign(member_id)
@action
def unassign_card(self, card_id: str, member_id: str):
"""
Un-assign a card.
:param card_id: Card ID
:param member_id: Member ID
"""
client = self._get_client()
card = client.get_card(card_id)
card.unassign(member_id)
@action
def attach_card(
self,
card_id: str,
name: Optional[str] = None,
mime_type: Optional[str] = None,
file: Optional[str] = None,
url: Optional[str] = None,
):
"""
Add an attachment to a card. It can be either a local file or a remote URL.
:param card_id: Card ID
:param name: File name
:param mime_type: MIME type
:param file: Path to the file
:param url: URL to the file
"""
client = self._get_client()
card = client.get_card(card_id)
card.attach(name=name, mimeType=mime_type, file=file, url=url)
@action
def remove_attachment(self, card_id: str, attachment_id: str):
"""
Remove an attachment from a card.
:param card_id: Card ID
:param attachment_id: Attachment ID
"""
client = self._get_client()
card = client.get_card(card_id)
card.remove_attachment(attachment_id)
@action
def change_card_board(
self,
card_id: str,
board: str,
list: Optional[str] = None, # pylint: disable=redefined-builtin
):
"""
Move a card to a new board.
:param card_id: Card ID
:param board: New board ID or name
:param list: Optional target list ID or name
"""
client = self._get_client()
card = client.get_card(card_id)
board_id = self._get_board(board).id
list_id = None
if list:
list_id = self._get_list(board_id, list).id
card.change_board(board_id, list_id)
@action
def change_card_list(
self, card_id: str, list: str # pylint: disable=redefined-builtin
):
"""
Move a card to a new list.
:param card_id: Card ID
:param list: List ID or name
"""
client = self._get_client()
card = client.get_card(card_id)
card.change_list(self._get_list(card.board.id, list).id)
@action
def change_card_pos(self, card_id: str, position: int):
"""
Move a card to a new position.
:param card_id: Card ID
:param position: New position index
"""
client = self._get_client()
card = client.get_card(card_id)
card.change_pos(position)
@action
def comment_card(self, card_id: str, text: str):
"""
Add a comment to a card.
:param card_id: Card ID
:param text: Comment text
"""
client = self._get_client()
card = client.get_card(card_id)
card.comment(text)
@action
def update_comment(self, card_id: str, comment_id: str, text: str):
"""
Update the content of a comment.
:param card_id: Card ID
:param comment_id: Comment ID
:param text: New comment text
"""
client = self._get_client()
card = client.get_card(card_id)
card.update_comment(comment_id, text)
@action
def delete_comment(self, card_id: str, comment_id: str):
"""
Delete a comment.
:param card_id: Card ID
:param comment_id: Comment ID
"""
client = self._get_client()
card = client.get_card(card_id)
card.delete_comment(comment_id)
@action
def set_card_name(self, card_id: str, name: str):
"""
Change the name of a card.
:param card_id: Card ID
:param name: New name
"""
client = self._get_client()
card = client.get_card(card_id)
card.set_name(name)
@action
def set_card_description(self, card_id: str, description: str):
"""
Change the description of a card.
:param card_id: Card ID
:param description: New description
"""
client = self._get_client()
card = client.get_card(card_id)
card.set_description(description)
@action
def add_card_member(self, card_id: str, member_id: str):
"""
Add a member to a card.
:param card_id: Card ID
:param member_id: Member ID
"""
client = self._get_client()
card = client.get_card(card_id)
card.add_member(member_id)
@action
def remove_card_member(self, card_id: str, member_id: str):
"""
Remove a member from a card.
:param card_id: Card ID
:param member_id: Member ID
"""
client = self._get_client()
card = client.get_card(card_id)
card.remove_member(member_id)
@action
def set_card_due(self, card_id: str, due: Union[str, datetime.datetime]):
"""
Set the due date for a card.
:param card_id: Card ID
:param due: Due date, as a datetime.datetime object or an ISO string
"""
client = self._get_client()
card = client.get_card(card_id)
if isinstance(due, str):
due = datetime.datetime.fromisoformat(due)
card.set_due(due)
@action
def remove_card_due(self, card_id: str):
"""
Remove the due date from a card.
:param card_id: Card ID
"""
client = self._get_client()
card = client.get_card(card_id)
card.remove_due()
@action
def set_card_due_complete(self, card_id: str):
"""
Set the due date of a card as completed.
:param card_id: Card ID
"""
client = self._get_client()
card = client.get_card(card_id)
card.set_due_complete()
@action
def remove_card_due_complete(self, card_id: str):
"""
Remove the due complete flag from a card.
:param card_id: Card ID
"""
client = self._get_client()
card = client.get_card(card_id)
card.remove_due_complete()
@action
def card_subscribe(self, card_id: str):
"""
Subscribe to a card.
:param card_id: Card ID
"""
client = self._get_client()
card = client.get_card(card_id)
card.subscribe()
@action
def get_cards(
self,
board: str,
list: Optional[str] = None, # pylint: disable=redefined-builtin
all: bool = False, # pylint: disable=redefined-builtin
):
"""
Get the list of cards on a board.
:param board: Board ID or name
:param list: List ID or name. If set then the method will only return the cards found on that list
(default: None)
:param all: If True, return all the cards included those that have been closed/archived/deleted. Otherwise,
only return open/active cards (default: False).
"""
b = self._get_board(board)
lists: Dict[str, TrelloList] = {
ll.id: TrelloList(
id=ll.id, name=ll.name, closed=ll.closed, subscribed=ll.subscribed
)
for ll in b.list_lists()
}
list_id = None
if list:
if list in lists:
list_id = list
else:
ll = next(
iter(
l1 for l1 in lists.values() if l1.name == list # type: ignore
),
None,
)
assert ll, f'No such list ID/name: {list}'
list_id = ll.id # type: ignore
return TrelloCardSchema().dump(
[
TrelloCard(
id=c.id,
name=c.name,
url=c.url,
closed=c.closed,
list=lists.get(c.list_id),
board=TrelloBoard(
id=c.board.id,
name=c.board.name,
url=c.board.url,
closed=c.board.closed,
description=c.board.description,
date_last_activity=c.board.date_last_activity,
),
attachments=[
TrelloAttachment(
id=a.get('id'),
url=a.get('url'),
size=a.get('bytes'),
date=a.get('date'),
edge_color=a.get('edgeColor'),
member_id=a.get('idMember'),
is_upload=a.get('isUpload'),
name=a.get('name'),
previews=[
TrelloPreview(
id=p.get('id'),
scaled=p.get('scaled'),
url=p.get('url'),
size=p.get('bytes'),
height=p.get('height'),
width=p.get('width'),
)
for p in a.get('previews', [])
],
mime_type=a.get('mimeType'),
)
for a in c.attachments
],
checklists=[
TrelloChecklist(
id=ch.id,
name=ch.name,
items=[
TrelloChecklistItem(
id=i.get('id'),
name=i.get('name'),
checked=i.get('checked'),
)
for i in ch.items
],
)
for ch in c.checklists
],
comments=[
TrelloComment(
id=co.get('id'),
text=co.get('data', {}).get('text'),
type=co.get('type'),
date=co.get('date'),
creator=TrelloUser(
id=co.get('memberCreator', {}).get('id'),
username=co.get('memberCreator', {}).get('username'),
fullname=co.get('memberCreator', {}).get('fullName'),
initials=co.get('memberCreator', {}).get('initials'),
avatar_url=co.get('memberCreator', {}).get('avatarUrl'),
),
)
for co in c.comments
],
labels=[
TrelloLabel(id=lb.id, name=lb.name, color=lb.color)
for lb in (c.labels or [])
],
is_due_complete=c.is_due_complete,
due_date=c.due_date,
description=c.description,
latest_card_move_date=c.latestCardMove_date,
date_last_activity=c.date_last_activity,
)
for c in b.all_cards()
if ((all or not c.closed) and (not list or c.list_id == list_id))
],
many=True,
)
def _initialize_connection(self, ws: WebSocketApp):
boards = [
b
for b in (self._get_boards() or [])
if not self._monitored_boards
or b.id in self._monitored_boards
or b.name in self._monitored_boards
]
for b in boards:
self._boards_by_id[b.id] = b
self._boards_by_name[b.name] = b
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, *args): # pylint: disable=too-many-return-statements
if len(args) < 2:
self.logger.warning(
'Missing websocket argument - make sure that you are using '
'a version of websocket-client < 0.53.0 or >= 0.58.0'
)
return
ws, msg = args[:2]
if not msg:
# Reply back with an empty message when the server sends an empty message
ws.send('')
return
try:
msg = json.loads(msg)
except Exception as e:
self.logger.warning(
'Received invalid JSON message from Trello: %s: %s', msg, e
)
return
if 'error' in msg:
self.logger.warning('Trello error: %s', 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':
get_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'],
}
)
get_bus().post(MoveCardEvent(**args))
elif 'closed' in delta['data'].get('old', {}):
cls = (
UnarchivedCardEvent
if delta['data']['old']['closed']
else ArchivedCardEvent
)
get_bus().post(cls(**args))
def _on_error(self, *args):
error = args[1] if len(args) > 1 else args[0]
self.logger.warning('Trello websocket error: %s', error)
def _on_close(self, *_):
self.logger.warning('Trello websocket connection closed')
self._connected.clear()
self._req_id = 0
while not self.should_stop():
try:
self._connect(reconnect=True)
self._connected.wait(timeout=10)
break
except TimeoutError:
continue
def _on_open(self, *args):
ws = args[0] if args else None
self._connected.set()
if ws:
self._send(ws, {'type': 'ping'})
self.logger.info('Trello websocket connected')
def _send(self, ws: WebSocketApp, msg: dict):
msg['reqid'] = self._req_id
ws.send(json.dumps(msg))
self._req_id += 1
def _connect(self, reconnect: bool = False) -> WebSocketApp:
assert self.url, 'Trello websocket URL not set'
if reconnect:
self.stop()
if not self._ws:
self._ws = WebSocketApp(
self.url,
header={
'Cookie': (
f'token={self.token}; '
f'cloud.session.token={self.cloud_session_token}'
)
},
on_open=self._on_open,
on_message=self._on_msg,
on_error=self._on_error,
on_close=self._on_close,
)
return self._ws
def main(self):
if not self.url:
self.logger.info(
"token/cloud_session_token not set: your Trello boards won't be monitored for changes"
)
self.wait_stop()
else:
ws = self._connect()
ws.run_forever()
def stop(self):
if self._ws:
self._ws.close()
self._ws = None
# vim:sw=4:ts=4:et: