diff --git a/config.json b/config.json index a90fa52..841d171 100644 --- a/config.json +++ b/config.json @@ -18,6 +18,10 @@ "token": "do_not_set_here_please_go_to_config_override", "board_id": "do_not_set_here_please_go_to_config_override" }, + "focalboard": { + "token": "do_not_set_here_please_go_to_config_override", + "url": "do_not_set_here_please_go_to_config_override" + }, "sheets": { "api_key_path": "do_not_set_here_please_go_to_config_override", "authors_sheet_key": "do_not_set_here_please_go_to_config_override", diff --git a/src/app_context.py b/src/app_context.py index f4b8b45..98111df 100644 --- a/src/app_context.py +++ b/src/app_context.py @@ -15,6 +15,7 @@ from .tg.sender import TelegramSender from .tg.tg_client import TgClient from .trello.trello_client import TrelloClient +from .focalboard.focalboard_client import FocalboardClient from .utils.singleton import Singleton from .vk.vk_client import VkClient @@ -54,6 +55,9 @@ def __init__( self.trello_client = TrelloClient( trello_config=config_manager.get_trello_config() ) + self.focalboard_client = FocalboardClient( + focalboard_config=config_manager.get_focalboard_config() + ) self.facebook_client = FacebookClient( facebook_config=config_manager.get_facebook_config() ) diff --git a/src/bot.py b/src/bot.py index 55690f2..1f8c3f1 100644 --- a/src/bot.py +++ b/src/bot.py @@ -184,6 +184,12 @@ def init_handlers(self): direct_message_only(handlers.get_tasks_report_advanced), "получить список задач из Trello (расширенный)", ) + self.add_manager_handler( + "get_tasks_report_focalboard", + CommandCategories.SUMMARY, + direct_message_only(handlers.get_tasks_report_focalboard), + "получить список задач из Focalboard", + ) self.add_manager_handler( "get_articles_arts", CommandCategories.SUMMARY, diff --git a/src/config_manager.py b/src/config_manager.py index 02a8607..0851a1a 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -60,6 +60,9 @@ def get_latest_jobs_config(self): def get_trello_config(self): return self.get_latest_config().get(consts.TRELLO_CONFIG, {}) + def get_focalboard_config(self): + return self.get_latest_config().get(consts.FOCALBOARD_CONFIG, {}) + def get_telegram_config(self): return self.get_latest_config().get(consts.TELEGRAM_CONFIG, {}) diff --git a/src/consts.py b/src/consts.py index c61c896..03b5062 100644 --- a/src/consts.py +++ b/src/consts.py @@ -37,6 +37,7 @@ class AppSource(Enum): # Upper level config keys TELEGRAM_CONFIG = "telegram" TRELLO_CONFIG = "trello" +FOCALBOARD_CONFIG = "focalboard" SHEETS_CONFIG = "sheets" DRIVE_CONFIG = "drive" FACEBOOK_CONFIG = "facebook" @@ -166,6 +167,7 @@ class GetTasksReportData: LISTS = "lists" INTRO_TEXT = "introduction" INCLUDE_LABELS = "include_labels" + USE_FOCALBOARD = "use_focalboard" class ManageRemindersData: diff --git a/src/focalboard/focalboard_client.py b/src/focalboard/focalboard_client.py new file mode 100644 index 0000000..fc03af9 --- /dev/null +++ b/src/focalboard/focalboard_client.py @@ -0,0 +1,161 @@ +import json +import logging +from typing import List +from urllib.parse import quote, urljoin + +import requests + +from ..strings import load +from ..utils.singleton import Singleton +from ..trello import trello_objects as objects + +logger = logging.getLogger(__name__) + + +class FocalboardClient(Singleton): + def __init__(self, focalboard_config=None): + if self.was_initialized(): + return + + self._focalboard_config = focalboard_config + self._update_from_config() + logger.info("FocalboardClient successfully initialized") + + def get_boards_for_user(self, user_id=None): + _, data = self._make_request("api/v2/teams/0/boards") + boards = [objects.TrelloBoard.from_focalboard_dict(board) for board in data] + logger.debug(f"get_boards_for_user: {boards}") + return boards + + def get_lists(self, board_id): + # if not board_id: + # board_id = self.board_id + # TODO make it more efficient + # essentially all list information is already passed via boards handler + _, data = self._make_request(f"api/v2/teams/0/boards") + list_data = [ + prop + for prop in [board for board in data if board["id"] == board_id][0][ + "cardProperties" + ] + if prop["name"] == "List" + ][0] + lists_data = list_data["options"] + lists = [ + objects.TrelloList.from_focalboard_dict(trello_list, board_id) + for trello_list in lists_data + ] + logger.debug(f"get_lists: {lists}") + return lists + + def get_list(self, board_id, list_id): + _, data = self._make_request(f"api/v2/teams/0/boards") + lists_data = [ + prop + for prop in [board for board in data if board["id"] == board_id][0][ + "cardProperties" + ] + if prop["name"] == "List" + ][0]["options"] + lst = [ + objects.TrelloList.from_focalboard_dict(trello_list, board_id) + for trello_list in lists_data + if trello_list["id"] == list_id + ][0] + logger.debug(f"get_list: {lst}") + return lst + + def _get_list_property(self, board_id): + _, data = self._make_request(f"api/v2/teams/0/boards") + return [ + prop + for prop in [board for board in data if board["id"] == board_id][0][ + "cardProperties" + ] + if prop["name"] == "List" + ][0]["id"] + + def _get_member_property(self, board_id): + _, data = self._make_request(f"api/v2/teams/0/boards") + return [ + prop + for prop in [board for board in data if board["id"] == board_id][0][ + "cardProperties" + ] + if prop["name"] == "Assignee" + ][0]["id"] + + def get_members(self, board_id) -> List[objects.TrelloMember]: + _, data = self._make_request(f"api/v2/boards/{board_id}/members") + members = [] + for member in data: + _, data = self._make_request(f"api/v2/users/{member['userId']}") + members.append(objects.TrelloMember.from_focalboard_dict(data)) + logger.debug(f"get_members: {members}") + return members + + def get_cards(self, list_ids, board_id): + _, data = self._make_request(f"api/v2/boards/{board_id}/blocks?all=true") + cards = [] + # TODO: move this to app state + members = self.get_members(board_id) + lists = self.get_lists(board_id) + list_prop = self._get_list_property(board_id) + member_prop = self._get_member_property(board_id) + view_id = [card_dict for card_dict in data if card_dict["type"] == "view"][0][ + "id" + ] + data = [ + card_dict + for card_dict in data + if card_dict["type"] == "card" + and card_dict["fields"]["properties"].get(list_prop, "") in list_ids + ] + for card_dict in data: + card = objects.TrelloCard.from_focalboard_dict(card_dict) + card.url = urljoin(self.url, f"{board_id}/{view_id}/{card.id}") + print(card.url) + # TODO: move this to app state + for trello_list in lists: + if trello_list.id == card_dict["fields"]["properties"].get( + list_prop, "" + ): + card.lst = trello_list + break + else: + logger.error(f"List name not found for {card}") + # TODO: move this to app state + if len(card_dict["fields"]["properties"].get(member_prop, [])) > 0: + for member in members: + if member.id in card_dict["fields"]["properties"].get( + member_prop, [] + ): + card.members.append(member) + if len(card.members) == 0: + logger.error(f"Member username not found for {card}") + cards.append(card) + logger.debug(f"get_cards: {cards}") + return cards + + def update_config(self, new_focalboard_config): + """To be called after config automatic update""" + self._focalboard_config = new_focalboard_config + self._update_from_config() + + def _update_from_config(self): + """Update attributes according to current self._focalboard_config""" + self.token = self._focalboard_config["token"] + self.url = self._focalboard_config["url"] + self.headers = { + "Accept": "application/json", + "Content-Type": "application/json", + "Authorization": f"Bearer {self.token}", + "X-Requested-With": "XMLHttpRequest", + } + + def _make_request(self, uri, payload={}): + response = requests.get( + urljoin(self.url, uri), params=payload, headers=self.headers + ) + logger.debug(f"{response.url}") + return response.status_code, response.json() diff --git a/src/jobs/config_updater_job.py b/src/jobs/config_updater_job.py index 9858f38..b44a644 100644 --- a/src/jobs/config_updater_job.py +++ b/src/jobs/config_updater_job.py @@ -58,6 +58,10 @@ def _execute( app_context.trello_client.update_config( job_scheduler.config_manager.get_trello_config() ) + # update config['focalboard'] + app_context.focalboard_client.update_config( + job_scheduler.config_manager.get_focalboard_config() + ) # update config['sheets'] app_context.sheets_client.update_config( job_scheduler.config_manager.get_sheets_config() diff --git a/src/tg/handlers/__init__.py b/src/tg/handlers/__init__.py index f808347..bed2fc5 100644 --- a/src/tg/handlers/__init__.py +++ b/src/tg/handlers/__init__.py @@ -25,7 +25,9 @@ # Admin (developer) handlers from .get_roles_for_member_handler import get_roles_for_member -from .get_tasks_report_handler import get_tasks_report, get_tasks_report_advanced +from .get_tasks_report_handler import ( + get_tasks_report, get_tasks_report_advanced, get_tasks_report_focalboard +) from .help_handler import help from .list_chats_handler import list_chats from .list_job_handler import list_jobs diff --git a/src/tg/handlers/get_tasks_report_handler.py b/src/tg/handlers/get_tasks_report_handler.py index 2af8292..9e8c049 100644 --- a/src/tg/handlers/get_tasks_report_handler.py +++ b/src/tg/handlers/get_tasks_report_handler.py @@ -24,6 +24,13 @@ def get_tasks_report(update: telegram.Update, tg_context: telegram.ext.CallbackC return +@manager_only +def get_tasks_report_focalboard(update: telegram.Update, tg_context: telegram.ext.CallbackContext): + _get_task_report_base(update, tg_context, advanced=False, use_focalboard=True) + + return + + @manager_only def get_tasks_report_advanced( update: telegram.Update, tg_context: telegram.ext.CallbackContext @@ -34,11 +41,17 @@ def get_tasks_report_advanced( def _get_task_report_base( - update: telegram.Update, tg_context: telegram.ext.CallbackContext, advanced: bool + update: telegram.Update, + tg_context: telegram.ext.CallbackContext, + advanced: bool, + use_focalboard: bool = False ): app_context = AppContext() - boards_list = app_context.trello_client.get_boards_for_user() + if use_focalboard: + boards_list = app_context.focalboard_client.get_boards_for_user() + else: + boards_list = app_context.trello_client.get_boards_for_user() boards_list_formatted = "\n".join( [f"{i + 1}) {brd.name}" for i, brd in enumerate(boards_list)] ) @@ -47,6 +60,7 @@ def _get_task_report_base( tg_context.chat_data[consts.GetTasksReportData.LISTS] = [ lst.to_dict() for lst in boards_list ] + tg_context.chat_data[consts.GetTasksReportData.USE_FOCALBOARD] = use_focalboard tg_context.chat_data[TASK_NAME] = { consts.NEXT_ACTION: consts.PlainTextUserAction.GET_TASKS_REPORT__ENTER_BOARD_NUMBER.value } @@ -60,15 +74,22 @@ def _get_task_report_base( def generate_report_messages( - board_id: str, list_id: str, introduction: str, add_labels: bool + board_id: str, list_id: str, introduction: str, add_labels: bool, use_focalboard: bool ) -> List[str]: app_context = AppContext() paragraphs = [] # list of paragraph strings - trello_list = app_context.trello_client.get_list(list_id) + if use_focalboard: + trello_list = app_context.focalboard_client.get_list(board_id, list_id) + else: + trello_list = app_context.trello_client.get_list(list_id) paragraphs.append(load("common__bold_wrapper", arg=trello_list.name)) - list_cards = app_context.trello_client.get_cards([list_id], board_id) + if use_focalboard: + list_cards = app_context.focalboard_client.get_cards([list_id], board_id) + else: + list_cards = app_context.trello_client.get_cards([list_id], board_id) + print(list_cards) paragraphs += _create_paragraphs_from_cards( list_cards, introduction, add_labels, app_context ) diff --git a/src/tg/handlers/user_message_handler.py b/src/tg/handlers/user_message_handler.py index 72a0380..7e0fa5f 100644 --- a/src/tg/handlers/user_message_handler.py +++ b/src/tg/handlers/user_message_handler.py @@ -10,6 +10,7 @@ from ...strings import load from ...tg.handlers import get_tasks_report_handler from ...trello.trello_client import TrelloClient +from ...focalboard.focalboard_client import FocalboardClient from .utils import get_chat_id, get_chat_name, get_sender_id, reply logger = logging.getLogger(__name__) @@ -84,13 +85,19 @@ def handle_user_message( return elif next_action == PlainTextUserAction.GET_TASKS_REPORT__ENTER_BOARD_NUMBER: trello_client = TrelloClient() + focalboard_client = FocalboardClient() try: board_list = tg_context.chat_data[consts.GetTasksReportData.LISTS] + use_focalboard = tg_context.chat_data[consts.GetTasksReportData.USE_FOCALBOARD] list_idx = int(user_input) - 1 assert 0 <= list_idx < len(board_list) board_id = board_list[list_idx]["id"] - trello_lists = trello_client.get_lists(board_id) - trello_lists = trello_lists[::-1] + if use_focalboard: + trello_lists = focalboard_client.get_lists(board_id) + trello_lists = trello_lists[::-1] + else: + trello_lists = trello_client.get_lists(board_id) + trello_lists = trello_lists[::-1] except Exception as e: logger.warning(e) reply( @@ -103,6 +110,7 @@ def handle_user_message( return command_data[consts.GetTasksReportData.BOARD_ID] = board_id + command_data[consts.GetTasksReportData.USE_FOCALBOARD] = use_focalboard command_data[consts.GetTasksReportData.LISTS] = [ lst.to_dict() for lst in trello_lists ] @@ -552,8 +560,9 @@ def handle_task_report(command_data, add_labels, update): board_id = command_data[consts.GetTasksReportData.BOARD_ID] list_id = command_data[consts.GetTasksReportData.LIST_ID] introduction = command_data[consts.GetTasksReportData.INTRO_TEXT] + use_focalboard = command_data[consts.GetTasksReportData.USE_FOCALBOARD] messages = get_tasks_report_handler.generate_report_messages( - board_id, list_id, introduction, add_labels + board_id, list_id, introduction, add_labels, use_focalboard=use_focalboard ) for message in messages: reply(message, update) diff --git a/src/trello/trello_objects.py b/src/trello/trello_objects.py index f33aac6..8c8804b 100644 --- a/src/trello/trello_objects.py +++ b/src/trello/trello_objects.py @@ -38,6 +38,18 @@ def from_dict(cls, data): logger.error(f"Bad board json {data}: {e}") return board + @classmethod + def from_focalboard_dict(cls, data): + board = cls() + try: + board.id = data["id"] + board.name = html.escape(data["title"]) + # board.url = data["shortUrl"] + except Exception as e: + board._ok = False + logger.error(f"Bad board json {data}: {e}") + return board + def to_dict(self): return { "id": self.id, @@ -112,6 +124,18 @@ def from_dict(cls, data): logger.error(f"Bad list json {data}: {e}") return trello_list + @classmethod + def from_focalboard_dict(cls, data, board_id): + trello_list = cls() + try: + trello_list.id = data["id"] + trello_list.name = html.escape(data["value"]) + trello_list.board_id = board_id + except Exception as e: + trello_list._ok = False + logger.error(f"Bad list json {data}: {e}") + return trello_list + def to_dict(self): return { "id": self.id, @@ -205,6 +229,22 @@ def from_dict(cls, data): logger.error(f"Bad card json {data}: {e}") return card + @classmethod + def from_focalboard_dict(cls, data): + card = cls() + try: + card.id = data["id"] + card.name = html.escape(data["title"]) + # card.labels = [TrelloCardLabel.from_dict(label) for label in data["labels"]] + # card.url = data["shortUrl"] + # card.due = ( + # datetime.strptime(data["due"], TIME_FORMAT) if data["due"] else None + # ) + except Exception as e: + card._ok = False + logger.error(f"Bad card json {data}: {e}") + return card + def to_dict(self): return { "id": self.id, @@ -461,6 +501,14 @@ def from_dict(cls, data): member.full_name = data["fullName"] return member + @classmethod + def from_focalboard_dict(cls, data): + member = cls() + member.id = data["id"] + member.username = data["username"] + member.full_name = data["username"] + return member + def to_dict(self): return { "id": self.id,