import json import time from platypush.backend import Backend from platypush.context import get_plugin from platypush.message.event.todoist import ( NewItemEvent, RemovedItemEvent, ModifiedItemEvent, CheckedItemEvent, ItemContentChangeEvent, TodoistSyncRequiredEvent, ) from platypush.plugins.todoist import TodoistPlugin class TodoistBackend(Backend): """ This backend listens for events on a Todoist account. """ def __init__(self, api_token: str = None, **kwargs): super().__init__(**kwargs) self._plugin: TodoistPlugin = get_plugin('todoist') if not api_token: assert ( self._plugin and self._plugin.api_token ), 'No api_token specified either on Todoist backend or plugin' self.api_token = self._plugin.api_token else: self.api_token = api_token self.url = self._plugin.get_user().output['websocket_url'] self._ws = None self._connected = False self._todoist_initialized = False self._items = {} self._event_handled = False def _on_msg(self): def hndl(*args): msg = args[1] if len(args) > 1 else args[0] msg = json.loads(msg) if msg.get('type') == 'sync_needed': self._refresh_all() return hndl def _retry_hndl(self): self._ws = None self.logger.warning('Todoist websocket connection closed') self._connected = False while not self._connected: self._connect() time.sleep(10) def _on_error(self): def hndl(*args): error = args[1] if len(args) > 1 else args[0] self.logger.warning('Todoist websocket error: {}'.format(error)) self._retry_hndl() return hndl def _on_close(self): def hndl(*_): self.logger.info('Todoist websocket closed') self._retry_hndl() return hndl def _on_open(self): # noinspection PyUnusedLocal def hndl(ws): self._connected = True self.logger.info('Todoist websocket connected') if not self._todoist_initialized: self._refresh_all() self._todoist_initialized = True return hndl def _connect(self): import websocket if not self._ws: self._ws = websocket.WebSocketApp( self.url, on_message=self._on_msg(), on_error=self._on_error(), on_close=self._on_close(), ) def _refresh_items(self): new_items = {i['id']: i for i in self._plugin.get_items().output} if self._todoist_initialized: for id, item in new_items.items(): if id not in self._items.keys(): self._event_handled = True self.bus.post(NewItemEvent(item)) for id, item in self._items.items(): if id not in new_items.keys(): self._event_handled = True self.bus.post(RemovedItemEvent(item)) elif new_items[id] != item: if new_items[id]['checked'] != item['checked']: self._event_handled = True self.bus.post(CheckedItemEvent(new_items[id])) elif new_items[id]['is_deleted'] != item['is_deleted']: self._event_handled = True self.bus.post(RemovedItemEvent(new_items[id])) elif new_items[id]['content'] != item['content']: self._event_handled = True self.bus.post(ItemContentChangeEvent(new_items[id])) else: self._event_handled = True self.bus.post(ModifiedItemEvent(new_items[id])) self._items = new_items def _refresh_all(self): self._event_handled = False self._plugin.sync() self._refresh_items() if not self._event_handled: self.bus.post(TodoistSyncRequiredEvent()) def run(self): super().run() self.logger.info('Started Todoist backend') self._connect() self._ws.on_open = self._on_open() self._ws.run_forever() # vim:sw=4:ts=4:et: