diff --git a/.github/workflows/publish_dev.yml b/.github/workflows/publish_dev.yml index 0eec3a0..76835dd 100644 --- a/.github/workflows/publish_dev.yml +++ b/.github/workflows/publish_dev.yml @@ -156,22 +156,22 @@ jobs: }' \ https://api.telegram.org/bot${{env.TELEGRAM_TEST_TOKEN}}/sendMessage - integration_tests: - needs: push - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - with: - ref: dev - - - name: Integration tests - uses: fylein/python-pytest-github-action@v2 - env: - CONFIG_OVERRIDE: ${{ secrets.CONFIG_OVERRIDE_TESTING }} - with: - args: | - apt-get update && apt-get install -y git && \ - pip3 install -e . && pip3 install -r requirements.txt && \ - pytest tests/integration -vs + # integration_tests: + # needs: push + + # runs-on: ubuntu-latest + + # steps: + # - uses: actions/checkout@v2 + # with: + # ref: dev + + # - name: Integration tests + # uses: fylein/python-pytest-github-action@v2 + # env: + # CONFIG_OVERRIDE: ${{ secrets.CONFIG_OVERRIDE_TESTING }} + # with: + # args: | + # apt-get update && apt-get install -y git && \ + # pip3 install -e . && pip3 install -r requirements.txt && \ + # pytest tests/integration -vs diff --git a/.github/workflows/publish_master.yml b/.github/workflows/publish_master.yml index fd6281c..6e5df23 100644 --- a/.github/workflows/publish_master.yml +++ b/.github/workflows/publish_master.yml @@ -158,22 +158,22 @@ jobs: -d '{"parse_mode": "markdown", "chat_id": ${{ env.TELEGRAM_ERROR_CHAT_ID }}, "text": "[github CI] deploy [failed](https://github.com/sysblok/sysblokbot/actions/runs/${{github.run_id}})"}' \ https://api.telegram.org/bot${{env.TELEGRAM_TOKEN}}/sendMessage - integration_tests: - needs: push - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - with: - ref: master - - - name: Integration tests - uses: fylein/python-pytest-github-action@v2 - env: - CONFIG_OVERRIDE: ${{ secrets.CONFIG_OVERRIDE_PROD }} - with: - args: | - apt-get update && apt-get install -y git && \ - pip3 install -e . && pip3 install -r requirements.txt && \ - pytest tests/integration -vs + # integration_tests: + # needs: push + + # runs-on: ubuntu-latest + + # steps: + # - uses: actions/checkout@v2 + # with: + # ref: master + + # - name: Integration tests + # uses: fylein/python-pytest-github-action@v2 + # env: + # CONFIG_OVERRIDE: ${{ secrets.CONFIG_OVERRIDE_PROD }} + # with: + # args: | + # apt-get update && apt-get install -y git && \ + # pip3 install -e . && pip3 install -r requirements.txt && \ + # pytest tests/integration -vs diff --git a/.github/workflows/publish_master_lobanov.yml b/.github/workflows/publish_master_lobanov.yml deleted file mode 100644 index 04175b8..0000000 --- a/.github/workflows/publish_master_lobanov.yml +++ /dev/null @@ -1,137 +0,0 @@ -name: Lobanov production - -on: - push: - # Publish `master` as Docker `prod` image. - branches: - - master - - # Publish `v1.2.3` tags as releases. - tags: - - v* - -env: - IMAGE_NAME: lobanovbot - -jobs: - # Push image to GitHub Packages. - # See also https://docs.docker.com/docker-hub/builds/ - push: - runs-on: ubuntu-latest - if: github.event_name == 'push' - - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.9.1 - with: - access_token: ${{ github.token }} - - - uses: actions/checkout@v2 - with: - ref: master - - - name: Generate build args - id: args - run: | - echo "::set-output name=commit_hash::$(git rev-parse HEAD)" - echo "::set-output name=commit_hash_short::$(git rev-parse --short HEAD)" - - - name: Publish to Github Packages Registry with cache - uses: whoan/docker-build-with-cache-action@v5 - env: - IMAGE_NAME: ${{ env.IMAGE_NAME }} - COMMIT_HASH: "${{ steps.args.outputs.commit_hash }}" - COMMIT_HASH_SHORT: "${{ steps.args.outputs.commit_hash_short }}" - with: - image_name: ${{ github.repository }}/${{ env.IMAGE_NAME }} - registry: docker.pkg.github.com - username: sysblok - password: ${{ secrets.GITHUB_TOKEN }} - dockerfile: Dockerfile - image_tag: "prod,latest" - build_extra_args: "--build-arg=COMMIT_HASH --build-arg=COMMIT_HASH_SHORT" - - - name: Notify us about failure - if: ${{ failure() }} - env: - TELEGRAM_PROD_TOKEN: ${{ secrets.TELEGRAM_LOBANOV_TOKEN }} - TELEGRAM_LOBANOV_LOG_CHAT_ID: ${{ secrets.TELEGRAM_LOBANOV_LOG_CHAT_ID }} - run: | - curl -X POST \ - -H 'Content-Type: application/json' \ - -d '{"parse_mode": "markdown", "chat_id": ${{ env.TELEGRAM_LOBANOV_LOG_CHAT_ID }}, "text": "[github CI] build [failed](https://github.com/sysblok/sysblokbot/actions/runs/${{github.run_id}})"}' \ - https://api.telegram.org/bot${{env.TELEGRAM_LOBANOV_TOKEN}}/sendMessage - - - name: Deploy package to digitalocean - uses: appleboy/ssh-action@master - env: - GITHUB_USERNAME: sysblok - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - IMAGE_NAME: ${{ env.IMAGE_NAME }} - ROOT_DIR: /home/${{ secrets.MASTER_DO_USER }}/lobanov - with: - host: ${{ secrets.MASTER_HOST }} - username: ${{ secrets.MASTER_DO_USER }} - passphrase: ${{ secrets.MASTER_DO_SSH_KEY_PASSWORD }} - key: ${{ secrets.MASTER_DO_SSH_KEY }} - port: ${{ secrets.MASTER_PORT }} - envs: GITHUB_USERNAME, GITHUB_TOKEN, IMAGE_NAME, ROOT_DIR - script: | - export CONTAINER_ID=$(docker ps -aq --filter name=lobanov) - export IMAGE_ID=$(docker images -aq --filter reference='docker.pkg.github.com/sysblok/sysblokbot/lobanov:prod') - docker stop --time=30 $CONTAINER_ID - docker rm $CONTAINER_ID - docker rmi $IMAGE_ID - docker login docker.pkg.github.com -u $GITHUB_USERNAME -p $GITHUB_TOKEN - touch ${{ env.ROOT_DIR }}/sysblokbot.sqlite - touch ${{ env.ROOT_DIR }}/strings.sqlite - docker run -dit --name lobanov \ - --env APP_SOURCE="github CI" --restart unless-stopped \ - -v ${{ env.ROOT_DIR }}/config_override.json:/app/config_override.json \ - -v ${{ env.ROOT_DIR }}/config_gs.json:/app/config_gs.json \ - -v ${{ env.ROOT_DIR }}/sysblokbot.sqlite:/app/sysblokbot.sqlite \ - -v ${{ env.ROOT_DIR }}/strings.sqlite:/app/strings.sqlite \ - -v ${{ env.ROOT_DIR }}/persistent_storage.pickle:/app/persistent_storage.pickle \ - docker.pkg.github.com/sysblok/sysblokbot/${{ env.IMAGE_NAME }}:prod - - - name: Notify us about failure - if: ${{ failure() }} - env: - TELEGRAM_PROD_TOKEN: ${{ secrets.TELEGRAM_LOBANOV_TOKEN }} - TELEGRAM_LOBANOV_LOG_CHAT_ID: ${{ secrets.TELEGRAM_LOBANOV_LOG_CHAT_ID }} - run: | - curl -X POST \ - -H 'Content-Type: application/json' \ - -d '{"parse_mode": "markdown", "chat_id": ${{ env.TELEGRAM_LOBANOV_LOG_CHAT_ID }}, "text": "[github CI] deploy [failed](https://github.com/sysblok/sysblokbot/actions/runs/${{github.run_id}})"}' \ - https://api.telegram.org/bot${{env.TELEGRAM_LOBANOV_TOKEN}}/sendMessage - - - name: Sleep for 30 seconds - uses: jakejarvis/wait-action@master - with: - time: '30s' - - - name: Check it has started properly - uses: appleboy/ssh-action@master - env: - TELEGRAM_PROD_TOKEN: ${{ secrets.TELEGRAM_LOBANOV_TOKEN }} - TELEGRAM_LOBANOV_LOG_CHAT_ID: ${{ secrets.TELEGRAM_LOBANOV_LOG_CHAT_ID }} - with: - host: ${{ secrets.MASTER_HOST }} - username: ${{ secrets.MASTER_DO_USER }} - passphrase: ${{ secrets.MASTER_DO_SSH_KEY_PASSWORD }} - key: ${{ secrets.MASTER_DO_SSH_KEY }} - port: ${{ secrets.MASTER_PORT }} - envs: TELEGRAM_PROD_TOKEN, TELEGRAM_LOBANOV_LOG_CHAT_ID - script: | - export CONTAINER_ID=$(docker ps -aq --filter name=lobanov) - export IS_RUNNING=$(docker container inspect -f '{{.State.Running}}' $CONTAINER_ID) - if [ $IS_RUNNING == "false" ]; then - export DOCKER_LOGS=$(docker logs $CONTAINER_ID) - curl -X POST \ - -H 'Content-Type: application/json' \ - -d '{"parse_mode": "markdown", "chat_id": ${{ env.TELEGRAM_LOBANOV_LOG_CHAT_ID }}, "text": "[github CI] deploy was ok but the bot [failed](https://github.com/sysblok/sysblokbot/actions/runs/${{github.run_id}})."}' \ - https://api.telegram.org/bot${{env.TELEGRAM_PROD_TOKEN}}/sendMessage - curl -X POST -H 'Content-Type: application/json' \ - -d '{"parse_mode": "markdown", "chat_id": ${{ env.TELEGRAM_LOBANOV_LOG_CHAT_ID }}, "text": "'"$(printf \'%q\n\' $DOCKER_LOGS)"'"}' \ - https://api.telegram.org/bot${{env.TELEGRAM_PROD_TOKEN}}/sendMessage - fi \ No newline at end of file diff --git a/.gitignore b/.gitignore index bac8fec..c584ca0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ config_override.json src/sheets/sysblokbot.json persistent_storage.pickle *.sqlite-journal +config_gs.json + +*.sqlite *.token diff --git a/src/bot.py b/src/bot.py index 66676ab..6cf724f 100644 --- a/src/bot.py +++ b/src/bot.py @@ -178,6 +178,12 @@ def init_handlers(self): direct_message_only(handlers.get_tasks_report), "получить список задач из Trello", ) + self.add_manager_handler( + "get_tasks_report_advanced", + CommandCategories.SUMMARY, + direct_message_only(handlers.get_tasks_report_advanced), + "получить список задач из Trello (расширенный)", + ) self.add_manager_handler( "get_articles_arts", CommandCategories.SUMMARY, @@ -389,6 +395,12 @@ def init_handlers(self): handlers.clean_chat_data, "clean_chat_data", ) + self.add_admin_handler( + "get_managers", + CommandCategories.MOST_USED, + handlers.get_managers, + "get_managers", + ) # sample handler self.add_handler( @@ -421,6 +433,12 @@ def init_handlers(self): self.admin_reply_handler("db_fetch_strings_sheet_job"), "обновить таблицу со строками из Google Sheets", ) + self.add_admin_handler( + "db_fetch_all_team_members", + CommandCategories.DATA_SYNC, + self.admin_reply_handler("db_fetch_all_team_members_job"), + "обновить таблицы всех пользователей (авторов, кураторов, команда) из Google Sheets", + ) # general purpose cmds self.add_admin_handler( diff --git a/src/facebook/facebook_client.py b/src/facebook/facebook_client.py index 82c1abf..eaa2ecc 100644 --- a/src/facebook/facebook_client.py +++ b/src/facebook/facebook_client.py @@ -6,12 +6,15 @@ import dateutil.parser as dateparser import facebook +import requests from ..consts import ReportPeriod from ..utils.singleton import Singleton from .facebook_objects import FacebookPage logger = logging.getLogger(__name__) +BASE_URL = 'https://graph.facebook.com' +API_VERSION = 'v10.0' class FacebookClient(Singleton): @@ -32,13 +35,20 @@ def _update_from_config(self): self._api_client = facebook.GraphAPI(self._facebook_config["token"], 7.0) self._page_id = self._facebook_config["page_id"] + def _make_graph_api_call(self, uri: str, params: dict) -> dict: + params['access_token'] = self._facebook_config["token"] + response = requests.get( + '/'.join([BASE_URL, API_VERSION, uri]) + '?' + + '&'.join(f"{key}={value}" for key, value in params.items())) + return response.json() + def get_page(self) -> FacebookPage: """ Get facebook page """ - page_dict = self._api_client.get_object( - self._page_id, fields="link,name,followers_count,fan_count" - ) + page_dict = self._make_graph_api_call(str(self._page_id), { + 'fields': 'link,name,followers_count,fan_count' + }) return FacebookPage.from_dict(page_dict) def get_new_posts_count(self, since: datetime, until: datetime) -> int: diff --git a/src/jobs/__init__.py b/src/jobs/__init__.py index 1423261..c213fa1 100644 --- a/src/jobs/__init__.py +++ b/src/jobs/__init__.py @@ -8,6 +8,7 @@ from .config_updater_job import ConfigUpdaterJob from .create_folders_for_illustrators_job import CreateFoldersForIllustratorsJob +from .db_fetch_all_team_members_job import DBFetchAllTeamMembersJob from .db_fetch_authors_sheet_job import DBFetchAuthorsSheetJob from .db_fetch_curators_sheet_job import DBFetchCuratorsSheetJob from .db_fetch_strings_sheet_job import DBFetchStringsSheetJob diff --git a/src/jobs/db_fetch_all_team_members_job.py b/src/jobs/db_fetch_all_team_members_job.py new file mode 100644 index 0000000..4ca968e --- /dev/null +++ b/src/jobs/db_fetch_all_team_members_job.py @@ -0,0 +1,34 @@ +import logging +from typing import Callable + +from ..app_context import AppContext +from ..strings import load +from .base_job import BaseJob + +logger = logging.getLogger(__name__) + + +class DBFetchAllTeamMembersJob(BaseJob): + @staticmethod + def _execute( + app_context: AppContext, send: Callable[[str], None], called_from_handler=False + ): + num_authors = app_context.db_client.fetch_authors_sheet( + app_context.sheets_client + ) + logger.info(f"Fetched {num_authors} authors") + send(load("db_fetch_authors_sheet_job__success", num_authors=num_authors)) + + num_curators = app_context.db_client.fetch_curators_sheet( + app_context.sheets_client + ) + logger.info(f"Fetched {num_curators} curators") + send(load("db_fetch_curators_sheet_job__success", num_curators=num_curators)) + + team_size = app_context.db_client.fetch_team_sheet( + app_context.sheets_client + ) + # after we fetch the team, we need to recalculate the roles + app_context.role_manager.calculate_db_roles() + logger.info(f"Fetched {team_size} team members") + send(load("db_fetch_team_sheet_job__success", team_size=team_size)) diff --git a/src/jobs/hr_acquisition_pt_job.py b/src/jobs/hr_acquisition_pt_job.py index c92a4c4..4812f78 100644 --- a/src/jobs/hr_acquisition_pt_job.py +++ b/src/jobs/hr_acquisition_pt_job.py @@ -58,7 +58,7 @@ def _process_raw_forms( for person in new_people: # filter out incomplete responses - if not person.telegram and not person.other_contacts: + if not person.telegram: person.status = load("sheets__hr__pt__raw__status_rejection") continue if person.telegram and ( diff --git a/src/jobs/site_health_check_job.py b/src/jobs/site_health_check_job.py index e6e7f6e..88e02d9 100644 --- a/src/jobs/site_health_check_job.py +++ b/src/jobs/site_health_check_job.py @@ -41,7 +41,18 @@ def _execute( kwargs = schedule[KWARGS] url = kwargs.get("index_url") logger.debug(f"Checking site health for {kwargs.get('name')}: {url}") - page = requests.get(url) + try: + page = requests.get(url) + except Exception as e: + send( + load( + "site_health_check_job__connection_error", + url=url, + ) + ) + logger.error(f"Connection error for {url}") + return + if page.status_code != 200: send( load( @@ -68,7 +79,7 @@ def _execute( logger.error(f"Bad body contents for {url}") logger.warning(f"Html:\n\n{body_contents}") return - logger.debug("Site contents look healthy") + logger.debug("Site content looks healthy") if called_from_handler: send( load( diff --git a/src/tg/handlers/__init__.py b/src/tg/handlers/__init__.py index 836efcc..a5f0a73 100644 --- a/src/tg/handlers/__init__.py +++ b/src/tg/handlers/__init__.py @@ -20,11 +20,12 @@ from .error_handler import error from .get_chat_data_handler import get_chat_data from .get_chat_id_handler import get_chat_id_handler as get_chat_id +from .get_managers_handler import get_managers from .get_members_for_role_handler import get_members_for_role # Admin (developer) handlers from .get_roles_for_member_handler import get_roles_for_member -from .get_tasks_report_handler import get_tasks_report +from .get_tasks_report_handler import get_tasks_report, get_tasks_report_advanced 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/access_config_handler.py b/src/tg/handlers/access_config_handler.py index 1b3a339..afad655 100644 --- a/src/tg/handlers/access_config_handler.py +++ b/src/tg/handlers/access_config_handler.py @@ -77,6 +77,14 @@ def reload_config_jobs(update, tg_context): reply(load("access_config_handler__reload_config_jobs_usage_example"), update) logger.warning(f"Failed to reload jobs config: {e}") return + num_strings = 0 + try: + num_strings = AppContext().strings_db_client.fetch_strings_sheet( + AppContext().sheets_client + ) + except Exception as e: + reply(load("access_config_handler__reload_config_jobs_usage_example"), update) + logger.warning(f"Failed to reload jobs config when fetching strings: {e}") reply( load( "common__code_wrapper", @@ -84,6 +92,10 @@ def reload_config_jobs(update, tg_context): ), update, ) + reply( + load("db_fetch_strings_sheet_job__success", num_strings=num_strings), + update, + ) @admin_only @@ -105,10 +117,12 @@ def add_manager(update, tg_context): try: tokens = update.message.text.strip().split(maxsplit=2) assert len(tokens) == 2 - manager_id = json.loads(tokens[1]) - assert isinstance(manager_id, int) or ( - isinstance(manager_id, str) and not manager_id.startswith("@") - ) + try: + manager_id = json.loads(tokens[1]) + except Exception: + manager_id = tokens[1] + manager_id = manager_id.lstrip("@") + assert isinstance(manager_id, int) or isinstance(manager_id, str) config_manager = ConfigManager() manager_ids = config_manager.get_telegram_config()[consts.TELEGRAM_MANAGER_IDS][ : diff --git a/src/tg/handlers/get_managers_handler.py b/src/tg/handlers/get_managers_handler.py new file mode 100644 index 0000000..6d35e35 --- /dev/null +++ b/src/tg/handlers/get_managers_handler.py @@ -0,0 +1,10 @@ +from ... import consts +from ...config_manager import ConfigManager +from .utils import admin_only, reply + + +@admin_only +def get_managers(update, tg_context): + config_manager = ConfigManager() + manager_ids = config_manager.get_telegram_config()[consts.TELEGRAM_MANAGER_IDS][:] + reply(str(manager_ids), update) diff --git a/src/tg/handlers/get_tasks_report_handler.py b/src/tg/handlers/get_tasks_report_handler.py index ddc2b67..2af8292 100644 --- a/src/tg/handlers/get_tasks_report_handler.py +++ b/src/tg/handlers/get_tasks_report_handler.py @@ -19,7 +19,23 @@ @manager_only def get_tasks_report(update: telegram.Update, tg_context: telegram.ext.CallbackContext): - # set initial dialogue data + _get_task_report_base(update, tg_context, advanced=False) + + return + + +@manager_only +def get_tasks_report_advanced( + update: telegram.Update, tg_context: telegram.ext.CallbackContext +): + _get_task_report_base(update, tg_context, advanced=True) + + return + + +def _get_task_report_base( + update: telegram.Update, tg_context: telegram.ext.CallbackContext, advanced: bool +): app_context = AppContext() boards_list = app_context.trello_client.get_boards_for_user() @@ -34,6 +50,7 @@ def get_tasks_report(update: telegram.Update, tg_context: telegram.ext.CallbackC tg_context.chat_data[TASK_NAME] = { consts.NEXT_ACTION: consts.PlainTextUserAction.GET_TASKS_REPORT__ENTER_BOARD_NUMBER.value } + tg_context.chat_data["advanced"] = advanced reply( load( "get_tasks_report_handler__choose_trello_board", lists=boards_list_formatted @@ -41,8 +58,6 @@ def get_tasks_report(update: telegram.Update, tg_context: telegram.ext.CallbackC update, ) - return - def generate_report_messages( board_id: str, list_id: str, introduction: str, add_labels: bool diff --git a/src/tg/handlers/user_message_handler.py b/src/tg/handlers/user_message_handler.py index 8364bdf..72a0380 100644 --- a/src/tg/handlers/user_message_handler.py +++ b/src/tg/handlers/user_message_handler.py @@ -90,6 +90,7 @@ def handle_user_message( 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] except Exception as e: logger.warning(e) reply( @@ -107,7 +108,10 @@ def handle_user_message( ] trello_lists_formatted = "\n".join( - [f"{i + 1}) {lst.name}" for i, lst in enumerate(trello_lists)] + [ + f"{len(trello_lists) - i}) {lst.name}" + for i, lst in enumerate(trello_lists) + ] ) reply( load( @@ -123,8 +127,8 @@ def handle_user_message( elif next_action == PlainTextUserAction.GET_TASKS_REPORT__ENTER_LIST_NUMBER: try: trello_lists = command_data.get(consts.GetTasksReportData.LISTS, []) - list_idx = int(user_input) - 1 - assert 0 <= list_idx < len(trello_lists) + list_idx = -int(user_input) + assert 0 > list_idx >= -len(trello_lists) list_id = trello_lists[list_idx]["id"] except Exception as e: logger.warning(e) @@ -148,6 +152,12 @@ def handle_user_message( ] ] ) + if not tg_context.chat_data.get("advanced"): + add_labels = button == ButtonValues.GET_TASKS_REPORT__LABELS__NO + command_data[consts.GetTasksReportData.INTRO_TEXT] = None + handle_task_report(command_data, add_labels, update) + return + reply( load("get_tasks_report_handler__enter_intro"), update, @@ -188,17 +198,7 @@ def handle_user_message( reply(load("user_message_handler__press_button_please"), update) return add_labels = button == ButtonValues.GET_TASKS_REPORT__LABELS__YES - board_id = command_data[consts.GetTasksReportData.BOARD_ID] - list_id = command_data[consts.GetTasksReportData.LIST_ID] - introduction = command_data[consts.GetTasksReportData.INTRO_TEXT] - messages = get_tasks_report_handler.generate_report_messages( - board_id, list_id, introduction, add_labels - ) - for message in messages: - reply(message, update) - # finished with last action for /trello_client_get_lists - set_next_action(command_data, None) - return + handle_task_report(command_data, add_labels, update) elif next_action == PlainTextUserAction.MANAGE_REMINDERS__CHOOSE_ACTION: if button is None: reply(load("user_message_handler__press_button_please"), update) @@ -546,3 +546,17 @@ def handle_new_members( # writes chat_id and chat name to db when anybody (including the bot) is added to a new chat # very heuristic solution DBClient().set_chat_name(get_chat_id(update), get_chat_name(update)) + + +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] + messages = get_tasks_report_handler.generate_report_messages( + board_id, list_id, introduction, add_labels + ) + for message in messages: + reply(message, update) + # finished with last action for /trello_client_get_lists + set_next_action(command_data, None) + return