From a44c9333d39d9af7f4e9eca5a8f7bd6ac3c81d26 Mon Sep 17 00:00:00 2001 From: Abhimanyu Yadav <122007096+Abhi1992002@users.noreply.github.com> Date: Thu, 30 Jan 2025 19:03:29 +0530 Subject: [PATCH] feat(block) : Todoist rest api blocks (#9369) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Resolves - #9303 and #9304 - Depends on - https://github.com/Significant-Gravitas/AutoGPT/pull/9319 ### Blocks list Block Name | What It Does | Manually Tested -- | -- | -- Todoist Create Label | Creates a new label in Todoist | ✅ Todoist List Labels | Retrieves all personal labels from Todoist | ✅ Todoist Get Label | Retrieves a specific label by ID | ✅ Todoist Create Task | Creates a new task in Todoist | ✅ Todoist Get Tasks | Retrieves active tasks from Todoist | ✅ Todoist Update Task | Updates an existing task | ✅ Todoist Close Task | Completes/closes a task | ✅ Todoist Reopen Task | Reopens a completed task | ✅ Todoist Delete Task | Permanently deletes a task | ✅ Todoist List Projects | Retrieves all projects from Todoist | ✅ Todoist Create Project | Creates a new project in Todoist | ✅ Todoist Get Project | Retrieves details for a specific project | ✅ Todoist Update Project | Updates an existing project | ✅ Todoist Delete Project | Deletes a project and its contents | ✅ Todoist List Collaborators | Retrieves collaborators on a project | ✅ Todoist List Sections | Retrieves sections from Todoist | ✅ Todoist Get Section | Retrieves details for a specific section | ✅ Todoist Delete Section | Deletes a section and its tasks | ✅ Todoist Create Comment | Creates a new comment on a task or project | ✅ Todoist Get Comments | Retrieves all comments for a task or project | ✅ Todoist Get Comment | Retrieves a specific comment by ID | ✅ Todoist Update Comment | Updates an existing comment | ✅ Todoist Delete Comment | Deletes a comment | ✅ > I’ve only created action blocks in Todoist because webhooks can only be manually created [we can't do it programatically right now]. I’ve already emailed Todoist for help, but they haven’t replied yet. Once I receive a reply, I’ll create a pull request for webhook triggers in Todoist. --- autogpt_platform/backend/.env.example | 8 + .../backend/backend/blocks/todoist/_auth.py | 61 ++ .../backend/backend/blocks/todoist/_types.py | 24 + .../backend/blocks/todoist/comments.py | 439 +++++++++++ .../backend/backend/blocks/todoist/labels.py | 557 ++++++++++++++ .../backend/blocks/todoist/projects.py | 566 ++++++++++++++ .../backend/blocks/todoist/sections.py | 306 ++++++++ .../backend/backend/blocks/todoist/tasks.py | 660 ++++++++++++++++ .../backend/blocks/twitter/tweets/manage.py | 3 +- .../backend/integrations/oauth/__init__.py | 3 + .../backend/integrations/oauth/todoist.py | 81 ++ .../backend/backend/integrations/providers.py | 1 + .../backend/backend/util/settings.py | 3 + autogpt_platform/backend/poetry.lock | 19 +- autogpt_platform/backend/pyproject.toml | 3 +- .../integrations/credentials-input.tsx | 1 + .../integrations/credentials-provider.tsx | 1 + .../src/lib/autogpt-server-api/types.ts | 1 + docs/content/platform/blocks/blocks.md | 29 +- docs/content/platform/blocks/todoist.md | 722 ++++++++++++++++++ .../platform/blocks/twitter/twitter.md | 321 ++++---- 21 files changed, 3673 insertions(+), 136 deletions(-) create mode 100644 autogpt_platform/backend/backend/blocks/todoist/_auth.py create mode 100644 autogpt_platform/backend/backend/blocks/todoist/_types.py create mode 100644 autogpt_platform/backend/backend/blocks/todoist/comments.py create mode 100644 autogpt_platform/backend/backend/blocks/todoist/labels.py create mode 100644 autogpt_platform/backend/backend/blocks/todoist/projects.py create mode 100644 autogpt_platform/backend/backend/blocks/todoist/sections.py create mode 100644 autogpt_platform/backend/backend/blocks/todoist/tasks.py create mode 100644 autogpt_platform/backend/backend/integrations/oauth/todoist.py create mode 100644 docs/content/platform/blocks/todoist.md diff --git a/autogpt_platform/backend/.env.example b/autogpt_platform/backend/.env.example index f0804681b5..b9267f4d4a 100644 --- a/autogpt_platform/backend/.env.example +++ b/autogpt_platform/backend/.env.example @@ -82,6 +82,14 @@ TWITTER_CLIENT_SECRET= LINEAR_CLIENT_ID= LINEAR_CLIENT_SECRET= +# To obtain Todoist API credentials: +# 1. Create a Todoist account at todoist.com +# 2. Visit the Developer Console: https://developer.todoist.com/appconsole.html +# 3. Click "Create new app" +# 4. Once created, copy your Client ID and Client Secret below +TODOIST_CLIENT_ID= +TODOIST_CLIENT_SECRET= + ## ===== OPTIONAL API KEYS ===== ## # LLM diff --git a/autogpt_platform/backend/backend/blocks/todoist/_auth.py b/autogpt_platform/backend/backend/blocks/todoist/_auth.py new file mode 100644 index 0000000000..0b05c63bbf --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/todoist/_auth.py @@ -0,0 +1,61 @@ +from typing import Literal + +from pydantic import SecretStr + +from backend.data.model import ( + CredentialsField, + CredentialsMetaInput, + OAuth2Credentials, + ProviderName, +) +from backend.integrations.oauth.todoist import TodoistOAuthHandler +from backend.util.settings import Secrets + +secrets = Secrets() +TODOIST_OAUTH_IS_CONFIGURED = bool( + secrets.todoist_client_id and secrets.todoist_client_secret +) + +TodoistCredentials = OAuth2Credentials +TodoistCredentialsInput = CredentialsMetaInput[ + Literal[ProviderName.TODOIST], Literal["oauth2"] +] + + +def TodoistCredentialsField(scopes: list[str]) -> TodoistCredentialsInput: + """ + Creates a Todoist credentials input on a block. + + Params: + scopes: The authorization scopes needed for the block to work. + """ + return CredentialsField( + required_scopes=set(TodoistOAuthHandler.DEFAULT_SCOPES + scopes), + description="The Todoist integration requires OAuth2 authentication.", + ) + + +TEST_CREDENTIALS = OAuth2Credentials( + id="01234567-89ab-cdef-0123-456789abcdef", + provider="todoist", + access_token=SecretStr("mock-todoist-access-token"), + refresh_token=None, + access_token_expires_at=None, + scopes=[ + "task:add", + "data:read", + "data:read_write", + "data:delete", + "project:delete", + ], + title="Mock Todoist OAuth2 Credentials", + username="mock-todoist-username", + refresh_token_expires_at=None, +) + +TEST_CREDENTIALS_INPUT = { + "provider": TEST_CREDENTIALS.provider, + "id": TEST_CREDENTIALS.id, + "type": TEST_CREDENTIALS.type, + "title": TEST_CREDENTIALS.title, +} diff --git a/autogpt_platform/backend/backend/blocks/todoist/_types.py b/autogpt_platform/backend/backend/blocks/todoist/_types.py new file mode 100644 index 0000000000..ecbdbd4bae --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/todoist/_types.py @@ -0,0 +1,24 @@ +from enum import Enum + + +class Colors(Enum): + berry_red = "berry_red" + red = "red" + orange = "orange" + yellow = "yellow" + olive_green = "olive_green" + lime_green = "lime_green" + green = "green" + mint_green = "mint_green" + teal = "teal" + sky_blue = "sky_blue" + light_blue = "light_blue" + blue = "blue" + grape = "grape" + violet = "violet" + lavender = "lavender" + magenta = "magenta" + salmon = "salmon" + charcoal = "charcoal" + grey = "grey" + taupe = "taupe" diff --git a/autogpt_platform/backend/backend/blocks/todoist/comments.py b/autogpt_platform/backend/backend/blocks/todoist/comments.py new file mode 100644 index 0000000000..fb4d3c017e --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/todoist/comments.py @@ -0,0 +1,439 @@ +from typing import Literal, Union + +from pydantic import BaseModel +from todoist_api_python.api import TodoistAPI +from typing_extensions import Optional + +from backend.blocks.todoist._auth import ( + TEST_CREDENTIALS, + TEST_CREDENTIALS_INPUT, + TodoistCredentials, + TodoistCredentialsField, + TodoistCredentialsInput, +) +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField + + +class TaskId(BaseModel): + discriminator: Literal["task"] + task_id: str + + +class ProjectId(BaseModel): + discriminator: Literal["project"] + project_id: str + + +class TodoistCreateCommentBlock(Block): + """Creates a new comment on a Todoist task or project""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + content: str = SchemaField(description="Comment content") + id_type: Union[TaskId, ProjectId] = SchemaField( + discriminator="discriminator", + description="Specify either task_id or project_id to comment on", + default=TaskId(discriminator="task", task_id=""), + advanced=False, + ) + attachment: Optional[dict] = SchemaField( + description="Optional file attachment", default=None + ) + + class Output(BlockSchema): + id: str = SchemaField(description="ID of created comment") + content: str = SchemaField(description="Comment content") + posted_at: str = SchemaField(description="Comment timestamp") + task_id: Optional[str] = SchemaField( + description="Associated task ID", default=None + ) + project_id: Optional[str] = SchemaField( + description="Associated project ID", default=None + ) + + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="1bba7e54-2310-4a31-8e6f-54d5f9ab7459", + description="Creates a new comment on a Todoist task or project", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistCreateCommentBlock.Input, + output_schema=TodoistCreateCommentBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "content": "Test comment", + "id_type": {"discriminator": "task", "task_id": "2995104339"}, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("id", "2992679862"), + ("content", "Test comment"), + ("posted_at", "2016-09-22T07:00:00.000000Z"), + ("task_id", "2995104339"), + ("project_id", None), + ], + test_mock={ + "create_comment": lambda content, credentials, task_id=None, project_id=None, attachment=None: { + "id": "2992679862", + "content": "Test comment", + "posted_at": "2016-09-22T07:00:00.000000Z", + "task_id": "2995104339", + "project_id": None, + } + }, + ) + + @staticmethod + def create_comment( + credentials: TodoistCredentials, + content: str, + task_id: Optional[str] = None, + project_id: Optional[str] = None, + attachment: Optional[dict] = None, + ): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + comment = api.add_comment( + content=content, + task_id=task_id, + project_id=project_id, + attachment=attachment, + ) + return comment.__dict__ + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + task_id = None + project_id = None + + if isinstance(input_data.id_type, TaskId): + task_id = input_data.id_type.task_id + else: + project_id = input_data.id_type.project_id + + comment_data = self.create_comment( + credentials, + input_data.content, + task_id=task_id, + project_id=project_id, + attachment=input_data.attachment, + ) + + if comment_data: + yield "id", comment_data["id"] + yield "content", comment_data["content"] + yield "posted_at", comment_data["posted_at"] + yield "task_id", comment_data["task_id"] + yield "project_id", comment_data["project_id"] + + except Exception as e: + yield "error", str(e) + + +class TodoistGetCommentsBlock(Block): + """Get all comments for a Todoist task or project""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + id_type: Union[TaskId, ProjectId] = SchemaField( + discriminator="discriminator", + description="Specify either task_id or project_id to get comments for", + default=TaskId(discriminator="task", task_id=""), + advanced=False, + ) + + class Output(BlockSchema): + comments: list = SchemaField(description="List of comments") + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="9972d8ae-ddf2-11ef-a9b8-32d3674e8b7e", + description="Get all comments for a Todoist task or project", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistGetCommentsBlock.Input, + output_schema=TodoistGetCommentsBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "id_type": {"discriminator": "task", "task_id": "2995104339"}, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ( + "comments", + [ + { + "id": "2992679862", + "content": "Test comment", + "posted_at": "2016-09-22T07:00:00.000000Z", + "task_id": "2995104339", + "project_id": None, + "attachment": None, + } + ], + ) + ], + test_mock={ + "get_comments": lambda credentials, task_id=None, project_id=None: [ + { + "id": "2992679862", + "content": "Test comment", + "posted_at": "2016-09-22T07:00:00.000000Z", + "task_id": "2995104339", + "project_id": None, + "attachment": None, + } + ] + }, + ) + + @staticmethod + def get_comments( + credentials: TodoistCredentials, + task_id: Optional[str] = None, + project_id: Optional[str] = None, + ): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + comments = api.get_comments(task_id=task_id, project_id=project_id) + return [comment.__dict__ for comment in comments] + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + task_id = None + project_id = None + + if isinstance(input_data.id_type, TaskId): + task_id = input_data.id_type.task_id + else: + project_id = input_data.id_type.project_id + + comments = self.get_comments( + credentials, task_id=task_id, project_id=project_id + ) + + yield "comments", comments + + except Exception as e: + yield "error", str(e) + + +class TodoistGetCommentBlock(Block): + """Get a single comment from Todoist using comment ID""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + comment_id: str = SchemaField(description="Comment ID to retrieve") + + class Output(BlockSchema): + content: str = SchemaField(description="Comment content") + id: str = SchemaField(description="Comment ID") + posted_at: str = SchemaField(description="Comment timestamp") + project_id: Optional[str] = SchemaField( + description="Associated project ID", default=None + ) + task_id: Optional[str] = SchemaField( + description="Associated task ID", default=None + ) + attachment: Optional[dict] = SchemaField( + description="Optional file attachment", default=None + ) + + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="a809d264-ddf2-11ef-9764-32d3674e8b7e", + description="Get a single comment from Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistGetCommentBlock.Input, + output_schema=TodoistGetCommentBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "comment_id": "2992679862", + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("content", "Test comment"), + ("id", "2992679862"), + ("posted_at", "2016-09-22T07:00:00.000000Z"), + ("project_id", None), + ("task_id", "2995104339"), + ("attachment", None), + ], + test_mock={ + "get_comment": lambda credentials, comment_id: { + "content": "Test comment", + "id": "2992679862", + "posted_at": "2016-09-22T07:00:00.000000Z", + "project_id": None, + "task_id": "2995104339", + "attachment": None, + } + }, + ) + + @staticmethod + def get_comment(credentials: TodoistCredentials, comment_id: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + comment = api.get_comment(comment_id=comment_id) + return comment.__dict__ + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + comment_data = self.get_comment( + credentials, comment_id=input_data.comment_id + ) + + if comment_data: + yield "content", comment_data["content"] + yield "id", comment_data["id"] + yield "posted_at", comment_data["posted_at"] + yield "project_id", comment_data["project_id"] + yield "task_id", comment_data["task_id"] + yield "attachment", comment_data["attachment"] + + except Exception as e: + yield "error", str(e) + + +class TodoistUpdateCommentBlock(Block): + """Updates a Todoist comment""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + comment_id: str = SchemaField(description="Comment ID to update") + content: str = SchemaField(description="New content for the comment") + + class Output(BlockSchema): + success: bool = SchemaField(description="Whether the update was successful") + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="b773c520-ddf2-11ef-9f34-32d3674e8b7e", + description="Updates a Todoist comment", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistUpdateCommentBlock.Input, + output_schema=TodoistUpdateCommentBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "comment_id": "2992679862", + "content": "Need one bottle of milk", + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("success", True)], + test_mock={"update_comment": lambda credentials, comment_id, content: True}, + ) + + @staticmethod + def update_comment(credentials: TodoistCredentials, comment_id: str, content: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + api.update_comment(comment_id=comment_id, content=content) + return True + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + success = self.update_comment( + credentials, + comment_id=input_data.comment_id, + content=input_data.content, + ) + + yield "success", success + + except Exception as e: + yield "error", str(e) + + +class TodoistDeleteCommentBlock(Block): + """Deletes a Todoist comment""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + comment_id: str = SchemaField(description="Comment ID to delete") + + class Output(BlockSchema): + success: bool = SchemaField(description="Whether the deletion was successful") + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="bda4c020-ddf2-11ef-b114-32d3674e8b7e", + description="Deletes a Todoist comment", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistDeleteCommentBlock.Input, + output_schema=TodoistDeleteCommentBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "comment_id": "2992679862", + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("success", True)], + test_mock={"delete_comment": lambda credentials, comment_id: True}, + ) + + @staticmethod + def delete_comment(credentials: TodoistCredentials, comment_id: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + success = api.delete_comment(comment_id=comment_id) + return success + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + success = self.delete_comment(credentials, comment_id=input_data.comment_id) + + yield "success", success + + except Exception as e: + yield "error", str(e) diff --git a/autogpt_platform/backend/backend/blocks/todoist/labels.py b/autogpt_platform/backend/backend/blocks/todoist/labels.py new file mode 100644 index 0000000000..f9b1b13ebf --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/todoist/labels.py @@ -0,0 +1,557 @@ +from todoist_api_python.api import TodoistAPI +from typing_extensions import Optional + +from backend.blocks.todoist._auth import ( + TEST_CREDENTIALS, + TEST_CREDENTIALS_INPUT, + TodoistCredentials, + TodoistCredentialsField, + TodoistCredentialsInput, +) +from backend.blocks.todoist._types import Colors +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField + + +class TodoistCreateLabelBlock(Block): + """Creates a new label in Todoist""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + name: str = SchemaField(description="Name of the label") + order: Optional[int] = SchemaField(description="Label order", default=None) + color: Optional[Colors] = SchemaField( + description="The color of the label icon", default=Colors.charcoal + ) + is_favorite: bool = SchemaField( + description="Whether the label is a favorite", default=False + ) + + class Output(BlockSchema): + id: str = SchemaField(description="ID of the created label") + name: str = SchemaField(description="Name of the label") + color: str = SchemaField(description="Color of the label") + order: int = SchemaField(description="Label order") + is_favorite: bool = SchemaField(description="Favorite status") + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="7288a968-de14-11ef-8997-32d3674e8b7e", + description="Creates a new label in Todoist, It will not work if same name already exists", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistCreateLabelBlock.Input, + output_schema=TodoistCreateLabelBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "name": "Test Label", + "color": Colors.charcoal.value, + "order": 1, + "is_favorite": False, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("id", "2156154810"), + ("name", "Test Label"), + ("color", "charcoal"), + ("order", 1), + ("is_favorite", False), + ], + test_mock={ + "create_label": lambda *args, **kwargs: { + "id": "2156154810", + "name": "Test Label", + "color": "charcoal", + "order": 1, + "is_favorite": False, + } + }, + ) + + @staticmethod + def create_label(credentials: TodoistCredentials, name: str, **kwargs): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + label = api.add_label(name=name, **kwargs) + return label.__dict__ + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + label_args = { + "order": input_data.order, + "color": ( + input_data.color.value if input_data.color is not None else None + ), + "is_favorite": input_data.is_favorite, + } + + label_data = self.create_label( + credentials, + input_data.name, + **{k: v for k, v in label_args.items() if v is not None}, + ) + + if label_data: + yield "id", label_data["id"] + yield "name", label_data["name"] + yield "color", label_data["color"] + yield "order", label_data["order"] + yield "is_favorite", label_data["is_favorite"] + + except Exception as e: + yield "error", str(e) + + +class TodoistListLabelsBlock(Block): + """Gets all personal labels from Todoist""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + + class Output(BlockSchema): + labels: list = SchemaField(description="List of complete label data") + label_ids: list = SchemaField(description="List of label IDs") + label_names: list = SchemaField(description="List of label names") + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="776dd750-de14-11ef-b927-32d3674e8b7e", + description="Gets all personal labels from Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistListLabelsBlock.Input, + output_schema=TodoistListLabelsBlock.Output, + test_input={"credentials": TEST_CREDENTIALS_INPUT}, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ( + "labels", + [ + { + "id": "2156154810", + "name": "Test Label", + "color": "charcoal", + "order": 1, + "is_favorite": False, + } + ], + ), + ("label_ids", ["2156154810"]), + ("label_names", ["Test Label"]), + ], + test_mock={ + "get_labels": lambda *args, **kwargs: [ + { + "id": "2156154810", + "name": "Test Label", + "color": "charcoal", + "order": 1, + "is_favorite": False, + } + ] + }, + ) + + @staticmethod + def get_labels(credentials: TodoistCredentials): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + labels = api.get_labels() + return [label.__dict__ for label in labels] + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + labels = self.get_labels(credentials) + yield "labels", labels + yield "label_ids", [label["id"] for label in labels] + yield "label_names", [label["name"] for label in labels] + + except Exception as e: + yield "error", str(e) + + +class TodoistGetLabelBlock(Block): + """Gets a personal label from Todoist by ID""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + label_id: str = SchemaField(description="ID of the label to retrieve") + + class Output(BlockSchema): + id: str = SchemaField(description="ID of the label") + name: str = SchemaField(description="Name of the label") + color: str = SchemaField(description="Color of the label") + order: int = SchemaField(description="Label order") + is_favorite: bool = SchemaField(description="Favorite status") + + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="7f236514-de14-11ef-bd7a-32d3674e8b7e", + description="Gets a personal label from Todoist by ID", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistGetLabelBlock.Input, + output_schema=TodoistGetLabelBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "label_id": "2156154810", + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("id", "2156154810"), + ("name", "Test Label"), + ("color", "charcoal"), + ("order", 1), + ("is_favorite", False), + ], + test_mock={ + "get_label": lambda *args, **kwargs: { + "id": "2156154810", + "name": "Test Label", + "color": "charcoal", + "order": 1, + "is_favorite": False, + } + }, + ) + + @staticmethod + def get_label(credentials: TodoistCredentials, label_id: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + label = api.get_label(label_id=label_id) + return label.__dict__ + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + label_data = self.get_label(credentials, input_data.label_id) + + if label_data: + yield "id", label_data["id"] + yield "name", label_data["name"] + yield "color", label_data["color"] + yield "order", label_data["order"] + yield "is_favorite", label_data["is_favorite"] + + except Exception as e: + yield "error", str(e) + + +class TodoistUpdateLabelBlock(Block): + """Updates a personal label in Todoist using ID""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + label_id: str = SchemaField(description="ID of the label to update") + name: Optional[str] = SchemaField( + description="New name of the label", default=None + ) + order: Optional[int] = SchemaField(description="Label order", default=None) + color: Optional[Colors] = SchemaField( + description="The color of the label icon", default=None + ) + is_favorite: bool = SchemaField( + description="Whether the label is a favorite (true/false)", default=False + ) + + class Output(BlockSchema): + success: bool = SchemaField(description="Whether the update was successful") + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="8755614c-de14-11ef-9b56-32d3674e8b7e", + description="Updates a personal label in Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistUpdateLabelBlock.Input, + output_schema=TodoistUpdateLabelBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "label_id": "2156154810", + "name": "Updated Label", + "color": Colors.charcoal.value, + "order": 2, + "is_favorite": True, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("success", True)], + test_mock={"update_label": lambda *args, **kwargs: True}, + ) + + @staticmethod + def update_label(credentials: TodoistCredentials, label_id: str, **kwargs): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + api.update_label(label_id=label_id, **kwargs) + return True + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + label_args = {} + if input_data.name is not None: + label_args["name"] = input_data.name + if input_data.order is not None: + label_args["order"] = input_data.order + if input_data.color is not None: + label_args["color"] = input_data.color.value + if input_data.is_favorite is not None: + label_args["is_favorite"] = input_data.is_favorite + + success = self.update_label( + credentials, + input_data.label_id, + **{k: v for k, v in label_args.items() if v is not None}, + ) + + yield "success", success + + except Exception as e: + yield "error", str(e) + + +class TodoistDeleteLabelBlock(Block): + """Deletes a personal label in Todoist""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + label_id: str = SchemaField(description="ID of the label to delete") + + class Output(BlockSchema): + success: bool = SchemaField(description="Whether the deletion was successful") + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="901b8f86-de14-11ef-98b8-32d3674e8b7e", + description="Deletes a personal label in Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistDeleteLabelBlock.Input, + output_schema=TodoistDeleteLabelBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "label_id": "2156154810", + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("success", True)], + test_mock={"delete_label": lambda *args, **kwargs: True}, + ) + + @staticmethod + def delete_label(credentials: TodoistCredentials, label_id: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + success = api.delete_label(label_id=label_id) + return success + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + success = self.delete_label(credentials, input_data.label_id) + yield "success", success + + except Exception as e: + yield "error", str(e) + + +class TodoistGetSharedLabelsBlock(Block): + """Gets all shared labels from Todoist""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + + class Output(BlockSchema): + labels: list = SchemaField(description="List of shared label names") + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="55fba510-de15-11ef-aed2-32d3674e8b7e", + description="Gets all shared labels from Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistGetSharedLabelsBlock.Input, + output_schema=TodoistGetSharedLabelsBlock.Output, + test_input={"credentials": TEST_CREDENTIALS_INPUT}, + test_credentials=TEST_CREDENTIALS, + test_output=[("labels", ["Label1", "Label2", "Label3"])], + test_mock={ + "get_shared_labels": lambda *args, **kwargs: [ + "Label1", + "Label2", + "Label3", + ] + }, + ) + + @staticmethod + def get_shared_labels(credentials: TodoistCredentials): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + labels = api.get_shared_labels() + return labels + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + labels = self.get_shared_labels(credentials) + yield "labels", labels + + except Exception as e: + yield "error", str(e) + + +class TodoistRenameSharedLabelsBlock(Block): + """Renames all instances of a shared label""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + name: str = SchemaField(description="The name of the existing label to rename") + new_name: str = SchemaField(description="The new name for the label") + + class Output(BlockSchema): + success: bool = SchemaField(description="Whether the rename was successful") + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="9d63ad9a-de14-11ef-ab3f-32d3674e8b7e", + description="Renames all instances of a shared label", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistRenameSharedLabelsBlock.Input, + output_schema=TodoistRenameSharedLabelsBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "name": "OldLabel", + "new_name": "NewLabel", + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("success", True)], + test_mock={"rename_shared_labels": lambda *args, **kwargs: True}, + ) + + @staticmethod + def rename_shared_labels(credentials: TodoistCredentials, name: str, new_name: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + success = api.rename_shared_label(name=name, new_name=new_name) + return success + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + success = self.rename_shared_labels( + credentials, input_data.name, input_data.new_name + ) + yield "success", success + + except Exception as e: + yield "error", str(e) + + +class TodoistRemoveSharedLabelsBlock(Block): + """Removes all instances of a shared label""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + name: str = SchemaField(description="The name of the label to remove") + + class Output(BlockSchema): + success: bool = SchemaField(description="Whether the removal was successful") + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="a6c5cbde-de14-11ef-8863-32d3674e8b7e", + description="Removes all instances of a shared label", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistRemoveSharedLabelsBlock.Input, + output_schema=TodoistRemoveSharedLabelsBlock.Output, + test_input={"credentials": TEST_CREDENTIALS_INPUT, "name": "LabelToRemove"}, + test_credentials=TEST_CREDENTIALS, + test_output=[("success", True)], + test_mock={"remove_shared_label": lambda *args, **kwargs: True}, + ) + + @staticmethod + def remove_shared_label(credentials: TodoistCredentials, name: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + success = api.remove_shared_label(name=name) + return success + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + success = self.remove_shared_label(credentials, input_data.name) + yield "success", success + + except Exception as e: + yield "error", str(e) diff --git a/autogpt_platform/backend/backend/blocks/todoist/projects.py b/autogpt_platform/backend/backend/blocks/todoist/projects.py new file mode 100644 index 0000000000..1b7300201b --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/todoist/projects.py @@ -0,0 +1,566 @@ +from todoist_api_python.api import TodoistAPI +from typing_extensions import Optional + +from backend.blocks.todoist._auth import ( + TEST_CREDENTIALS, + TEST_CREDENTIALS_INPUT, + TodoistCredentials, + TodoistCredentialsField, + TodoistCredentialsInput, +) +from backend.blocks.todoist._types import Colors +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField + + +class TodoistListProjectsBlock(Block): + """Gets all projects for a Todoist user""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + + class Output(BlockSchema): + names_list: list[str] = SchemaField(description="List of project names") + ids_list: list[str] = SchemaField(description="List of project IDs") + url_list: list[str] = SchemaField(description="List of project URLs") + complete_data: list[dict] = SchemaField( + description="Complete project data including all fields" + ) + error: str = SchemaField(description="Error message if request failed") + + def __init__(self): + super().__init__( + id="5f3e1d5b-6bc5-40e3-97ee-1318b3f38813", + description="Gets all projects and their details from Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistListProjectsBlock.Input, + output_schema=TodoistListProjectsBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("names_list", ["Inbox"]), + ("ids_list", ["220474322"]), + ("url_list", ["https://todoist.com/showProject?id=220474322"]), + ( + "complete_data", + [ + { + "id": "220474322", + "name": "Inbox", + "url": "https://todoist.com/showProject?id=220474322", + } + ], + ), + ], + test_mock={ + "get_project_lists": lambda *args, **kwargs: ( + ["Inbox"], + ["220474322"], + ["https://todoist.com/showProject?id=220474322"], + [ + { + "id": "220474322", + "name": "Inbox", + "url": "https://todoist.com/showProject?id=220474322", + } + ], + None, + ) + }, + ) + + @staticmethod + def get_project_lists(credentials: TodoistCredentials): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + projects = api.get_projects() + + names = [] + ids = [] + urls = [] + complete_data = [] + + for project in projects: + names.append(project.name) + ids.append(project.id) + urls.append(project.url) + complete_data.append(project.__dict__) + + return names, ids, urls, complete_data, None + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + names, ids, urls, data, error = self.get_project_lists(credentials) + + if names: + yield "names_list", names + if ids: + yield "ids_list", ids + if urls: + yield "url_list", urls + if data: + yield "complete_data", data + + except Exception as e: + yield "error", str(e) + + +class TodoistCreateProjectBlock(Block): + """Creates a new project in Todoist""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + name: str = SchemaField(description="Name of the project", advanced=False) + parent_id: Optional[str] = SchemaField( + description="Parent project ID", default=None, advanced=True + ) + color: Optional[Colors] = SchemaField( + description="Color of the project icon", + default=Colors.charcoal, + advanced=True, + ) + is_favorite: bool = SchemaField( + description="Whether the project is a favorite", + default=False, + advanced=True, + ) + view_style: Optional[str] = SchemaField( + description="Display style (list or board)", default=None, advanced=True + ) + + class Output(BlockSchema): + success: bool = SchemaField(description="Whether the creation was successful") + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="ade60136-de14-11ef-b5e5-32d3674e8b7e", + description="Creates a new project in Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistCreateProjectBlock.Input, + output_schema=TodoistCreateProjectBlock.Output, + test_input={"credentials": TEST_CREDENTIALS_INPUT, "name": "Test Project"}, + test_credentials=TEST_CREDENTIALS, + test_output=[("success", True)], + test_mock={"create_project": lambda *args, **kwargs: (True)}, + ) + + @staticmethod + def create_project( + credentials: TodoistCredentials, + name: str, + parent_id: Optional[str], + color: Optional[Colors], + is_favorite: bool, + view_style: Optional[str], + ): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + params = {"name": name, "is_favorite": is_favorite} + + if parent_id is not None: + params["parent_id"] = parent_id + if color is not None: + params["color"] = color.value + if view_style is not None: + params["view_style"] = view_style + + api.add_project(**params) + return True + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + success = self.create_project( + credentials=credentials, + name=input_data.name, + parent_id=input_data.parent_id, + color=input_data.color, + is_favorite=input_data.is_favorite, + view_style=input_data.view_style, + ) + + yield "success", success + + except Exception as e: + yield "error", str(e) + + +class TodoistGetProjectBlock(Block): + """Gets details for a specific Todoist project""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + project_id: str = SchemaField( + description="ID of the project to get details for", advanced=False + ) + + class Output(BlockSchema): + project_id: str = SchemaField(description="ID of project") + project_name: str = SchemaField(description="Name of project") + project_url: str = SchemaField(description="URL of project") + complete_data: dict = SchemaField( + description="Complete project data including all fields" + ) + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="b435b5ea-de14-11ef-8b51-32d3674e8b7e", + description="Gets details for a specific Todoist project", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistGetProjectBlock.Input, + output_schema=TodoistGetProjectBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "project_id": "2203306141", + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("project_id", "2203306141"), + ("project_name", "Shopping List"), + ("project_url", "https://todoist.com/showProject?id=2203306141"), + ( + "complete_data", + { + "id": "2203306141", + "name": "Shopping List", + "url": "https://todoist.com/showProject?id=2203306141", + }, + ), + ], + test_mock={ + "get_project": lambda *args, **kwargs: ( + "2203306141", + "Shopping List", + "https://todoist.com/showProject?id=2203306141", + { + "id": "2203306141", + "name": "Shopping List", + "url": "https://todoist.com/showProject?id=2203306141", + }, + ) + }, + ) + + @staticmethod + def get_project(credentials: TodoistCredentials, project_id: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + project = api.get_project(project_id=project_id) + + return project.id, project.name, project.url, project.__dict__ + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + project_id, project_name, project_url, data = self.get_project( + credentials=credentials, project_id=input_data.project_id + ) + + if project_id: + yield "project_id", project_id + if project_name: + yield "project_name", project_name + if project_url: + yield "project_url", project_url + if data: + yield "complete_data", data + + except Exception as e: + yield "error", str(e) + + +class TodoistUpdateProjectBlock(Block): + """Updates an existing project in Todoist""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + project_id: str = SchemaField( + description="ID of project to update", advanced=False + ) + name: Optional[str] = SchemaField( + description="New name for the project", default=None, advanced=False + ) + color: Optional[Colors] = SchemaField( + description="New color for the project icon", default=None, advanced=True + ) + is_favorite: Optional[bool] = SchemaField( + description="Whether the project should be a favorite", + default=None, + advanced=True, + ) + view_style: Optional[str] = SchemaField( + description="Display style (list or board)", default=None, advanced=True + ) + + class Output(BlockSchema): + success: bool = SchemaField(description="Whether the update was successful") + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="ba41a20a-de14-11ef-91d7-32d3674e8b7e", + description="Updates an existing project in Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistUpdateProjectBlock.Input, + output_schema=TodoistUpdateProjectBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "project_id": "2203306141", + "name": "Things To Buy", + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("success", True)], + test_mock={"update_project": lambda *args, **kwargs: (True)}, + ) + + @staticmethod + def update_project( + credentials: TodoistCredentials, + project_id: str, + name: Optional[str], + color: Optional[Colors], + is_favorite: Optional[bool], + view_style: Optional[str], + ): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + params = {} + + if name is not None: + params["name"] = name + if color is not None: + params["color"] = color.value + if is_favorite is not None: + params["is_favorite"] = is_favorite + if view_style is not None: + params["view_style"] = view_style + + api.update_project(project_id=project_id, **params) + return True + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + success = self.update_project( + credentials=credentials, + project_id=input_data.project_id, + name=input_data.name, + color=input_data.color, + is_favorite=input_data.is_favorite, + view_style=input_data.view_style, + ) + + yield "success", success + + except Exception as e: + yield "error", str(e) + + +class TodoistDeleteProjectBlock(Block): + """Deletes a project and all of its sections and tasks""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + project_id: str = SchemaField( + description="ID of project to delete", advanced=False + ) + + class Output(BlockSchema): + success: bool = SchemaField(description="Whether the deletion was successful") + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="c2893acc-de14-11ef-a113-32d3674e8b7e", + description="Deletes a Todoist project and all its contents", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistDeleteProjectBlock.Input, + output_schema=TodoistDeleteProjectBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "project_id": "2203306141", + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("success", True)], + test_mock={"delete_project": lambda *args, **kwargs: (True)}, + ) + + @staticmethod + def delete_project(credentials: TodoistCredentials, project_id: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + success = api.delete_project(project_id=project_id) + return success + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + success = self.delete_project( + credentials=credentials, project_id=input_data.project_id + ) + + yield "success", success + + except Exception as e: + yield "error", str(e) + + +class TodoistListCollaboratorsBlock(Block): + """Gets all collaborators for a Todoist project""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + project_id: str = SchemaField( + description="ID of the project to get collaborators for", advanced=False + ) + + class Output(BlockSchema): + collaborator_ids: list[str] = SchemaField( + description="List of collaborator IDs" + ) + collaborator_names: list[str] = SchemaField( + description="List of collaborator names" + ) + collaborator_emails: list[str] = SchemaField( + description="List of collaborator email addresses" + ) + complete_data: list[dict] = SchemaField( + description="Complete collaborator data including all fields" + ) + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="c99c804e-de14-11ef-9f47-32d3674e8b7e", + description="Gets all collaborators for a specific Todoist project", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistListCollaboratorsBlock.Input, + output_schema=TodoistListCollaboratorsBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "project_id": "2203306141", + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("collaborator_ids", ["2671362", "2671366"]), + ("collaborator_names", ["Alice", "Bob"]), + ("collaborator_emails", ["alice@example.com", "bob@example.com"]), + ( + "complete_data", + [ + { + "id": "2671362", + "name": "Alice", + "email": "alice@example.com", + }, + {"id": "2671366", "name": "Bob", "email": "bob@example.com"}, + ], + ), + ], + test_mock={ + "get_collaborators": lambda *args, **kwargs: ( + ["2671362", "2671366"], + ["Alice", "Bob"], + ["alice@example.com", "bob@example.com"], + [ + { + "id": "2671362", + "name": "Alice", + "email": "alice@example.com", + }, + {"id": "2671366", "name": "Bob", "email": "bob@example.com"}, + ], + ) + }, + ) + + @staticmethod + def get_collaborators(credentials: TodoistCredentials, project_id: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + collaborators = api.get_collaborators(project_id=project_id) + + ids = [] + names = [] + emails = [] + complete_data = [] + + for collaborator in collaborators: + ids.append(collaborator.id) + names.append(collaborator.name) + emails.append(collaborator.email) + complete_data.append(collaborator.__dict__) + + return ids, names, emails, complete_data + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + ids, names, emails, data = self.get_collaborators( + credentials=credentials, project_id=input_data.project_id + ) + + if ids: + yield "collaborator_ids", ids + if names: + yield "collaborator_names", names + if emails: + yield "collaborator_emails", emails + if data: + yield "complete_data", data + + except Exception as e: + yield "error", str(e) diff --git a/autogpt_platform/backend/backend/blocks/todoist/sections.py b/autogpt_platform/backend/backend/blocks/todoist/sections.py new file mode 100644 index 0000000000..bb0144a897 --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/todoist/sections.py @@ -0,0 +1,306 @@ +from todoist_api_python.api import TodoistAPI +from typing_extensions import Optional + +from backend.blocks.todoist._auth import ( + TEST_CREDENTIALS, + TEST_CREDENTIALS_INPUT, + TodoistCredentials, + TodoistCredentialsField, + TodoistCredentialsInput, +) +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField + + +class TodoistListSectionsBlock(Block): + """Gets all sections for a Todoist project""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + project_id: Optional[str] = SchemaField( + description="Optional project ID to filter sections" + ) + + class Output(BlockSchema): + names_list: list[str] = SchemaField(description="List of section names") + ids_list: list[str] = SchemaField(description="List of section IDs") + complete_data: list[dict] = SchemaField( + description="Complete section data including all fields" + ) + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="d6a116d8-de14-11ef-a94c-32d3674e8b7e", + description="Gets all sections and their details from Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistListSectionsBlock.Input, + output_schema=TodoistListSectionsBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "project_id": "2203306141", + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("names_list", ["Groceries"]), + ("ids_list", ["7025"]), + ( + "complete_data", + [ + { + "id": "7025", + "project_id": "2203306141", + "order": 1, + "name": "Groceries", + } + ], + ), + ], + test_mock={ + "get_section_lists": lambda *args, **kwargs: ( + ["Groceries"], + ["7025"], + [ + { + "id": "7025", + "project_id": "2203306141", + "order": 1, + "name": "Groceries", + } + ], + ) + }, + ) + + @staticmethod + def get_section_lists( + credentials: TodoistCredentials, project_id: Optional[str] = None + ): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + sections = api.get_sections(project_id=project_id) + + names = [] + ids = [] + complete_data = [] + + for section in sections: + names.append(section.name) + ids.append(section.id) + complete_data.append(section.__dict__) + + return names, ids, complete_data + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + names, ids, data = self.get_section_lists( + credentials, input_data.project_id + ) + + if names: + yield "names_list", names + if ids: + yield "ids_list", ids + if data: + yield "complete_data", data + + except Exception as e: + yield "error", str(e) + + +# Error in official todoist SDK. Will add this block using sync_api +# class TodoistCreateSectionBlock(Block): +# """Creates a new section in a Todoist project""" + +# class Input(BlockSchema): +# credentials: TodoistCredentialsInput = TodoistCredentialsField([]) +# name: str = SchemaField(description="Section name") +# project_id: str = SchemaField(description="Project ID this section should belong to") +# order: Optional[int] = SchemaField(description="Optional order among other sections", default=None) + +# class Output(BlockSchema): +# success: bool = SchemaField(description="Whether section was successfully created") +# error: str = SchemaField(description="Error message if the request failed") + +# def __init__(self): +# super().__init__( +# id="e3025cfc-de14-11ef-b9f2-32d3674e8b7e", +# description="Creates a new section in a Todoist project", +# categories={BlockCategory.PRODUCTIVITY}, +# input_schema=TodoistCreateSectionBlock.Input, +# output_schema=TodoistCreateSectionBlock.Output, +# test_input={ +# "credentials": TEST_CREDENTIALS_INPUT, +# "name": "Groceries", +# "project_id": "2203306141" +# }, +# test_credentials=TEST_CREDENTIALS, +# test_output=[ +# ("success", True) +# ], +# test_mock={ +# "create_section": lambda *args, **kwargs: ( +# {"id": "7025", "project_id": "2203306141", "order": 1, "name": "Groceries"}, +# ) +# }, +# ) + +# @staticmethod +# def create_section(credentials: TodoistCredentials, name: str, project_id: str, order: Optional[int] = None): +# try: +# api = TodoistAPI(credentials.access_token.get_secret_value()) +# section = api.add_section(name=name, project_id=project_id, order=order) +# return section.__dict__ + +# except Exception as e: +# raise e + +# def run( +# self, +# input_data: Input, +# *, +# credentials: TodoistCredentials, +# **kwargs, +# ) -> BlockOutput: +# try: +# section_data = self.create_section( +# credentials, +# input_data.name, +# input_data.project_id, +# input_data.order +# ) + +# if section_data: +# yield "success", True + +# except Exception as e: +# yield "error", str(e) + + +class TodoistGetSectionBlock(Block): + """Gets a single section from Todoist by ID""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + section_id: str = SchemaField(description="ID of section to fetch") + + class Output(BlockSchema): + id: str = SchemaField(description="ID of section") + project_id: str = SchemaField(description="Project ID the section belongs to") + order: int = SchemaField(description="Order of the section") + name: str = SchemaField(description="Name of the section") + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="ea5580e2-de14-11ef-a5d3-32d3674e8b7e", + description="Gets a single section by ID from Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistGetSectionBlock.Input, + output_schema=TodoistGetSectionBlock.Output, + test_input={"credentials": TEST_CREDENTIALS_INPUT, "section_id": "7025"}, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("id", "7025"), + ("project_id", "2203306141"), + ("order", 1), + ("name", "Groceries"), + ], + test_mock={ + "get_section": lambda *args, **kwargs: { + "id": "7025", + "project_id": "2203306141", + "order": 1, + "name": "Groceries", + } + }, + ) + + @staticmethod + def get_section(credentials: TodoistCredentials, section_id: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + section = api.get_section(section_id=section_id) + return section.__dict__ + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + section_data = self.get_section(credentials, input_data.section_id) + + if section_data: + yield "id", section_data["id"] + yield "project_id", section_data["project_id"] + yield "order", section_data["order"] + yield "name", section_data["name"] + + except Exception as e: + yield "error", str(e) + + +class TodoistDeleteSectionBlock(Block): + """Deletes a section and all its tasks from Todoist""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + section_id: str = SchemaField(description="ID of section to delete") + + class Output(BlockSchema): + success: bool = SchemaField( + description="Whether section was successfully deleted" + ) + error: str = SchemaField(description="Error message if the request failed") + + def __init__(self): + super().__init__( + id="f0e52eee-de14-11ef-9b12-32d3674e8b7e", + description="Deletes a section and all its tasks from Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistDeleteSectionBlock.Input, + output_schema=TodoistDeleteSectionBlock.Output, + test_input={"credentials": TEST_CREDENTIALS_INPUT, "section_id": "7025"}, + test_credentials=TEST_CREDENTIALS, + test_output=[("success", True)], + test_mock={"delete_section": lambda *args, **kwargs: (True)}, + ) + + @staticmethod + def delete_section(credentials: TodoistCredentials, section_id: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + success = api.delete_section(section_id=section_id) + return success + + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + success = self.delete_section(credentials, input_data.section_id) + yield "success", success + + except Exception as e: + yield "error", str(e) diff --git a/autogpt_platform/backend/backend/blocks/todoist/tasks.py b/autogpt_platform/backend/backend/blocks/todoist/tasks.py new file mode 100644 index 0000000000..594897fd8f --- /dev/null +++ b/autogpt_platform/backend/backend/blocks/todoist/tasks.py @@ -0,0 +1,660 @@ +from datetime import datetime + +from todoist_api_python.api import TodoistAPI +from todoist_api_python.models import Task +from typing_extensions import Optional + +from backend.blocks.todoist._auth import ( + TEST_CREDENTIALS, + TEST_CREDENTIALS_INPUT, + TodoistCredentials, + TodoistCredentialsField, + TodoistCredentialsInput, +) +from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema +from backend.data.model import SchemaField + + +class TodoistCreateTaskBlock(Block): + """Creates a new task in a Todoist project""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + content: str = SchemaField(description="Task content", advanced=False) + description: Optional[str] = SchemaField( + description="Task description", default=None, advanced=False + ) + project_id: Optional[str] = SchemaField( + description="Project ID this task should belong to", + default=None, + advanced=False, + ) + section_id: Optional[str] = SchemaField( + description="Section ID this task should belong to", + default=None, + advanced=False, + ) + parent_id: Optional[str] = SchemaField( + description="Parent task ID", default=None, advanced=True + ) + order: Optional[int] = SchemaField( + description="Optional order among other tasks,[Non-zero integer value used by clients to sort tasks under the same parent]", + default=None, + advanced=True, + ) + labels: Optional[list[str]] = SchemaField( + description="Task labels", default=None, advanced=True + ) + priority: Optional[int] = SchemaField( + description="Task priority from 1 (normal) to 4 (urgent)", + default=None, + advanced=True, + ) + due_date: Optional[datetime] = SchemaField( + description="Due date in YYYY-MM-DD format", advanced=True, default=None + ) + deadline_date: Optional[datetime] = SchemaField( + description="Specific date in YYYY-MM-DD format relative to user's timezone", + default=None, + advanced=True, + ) + assignee_id: Optional[str] = SchemaField( + description="Responsible user ID", default=None, advanced=True + ) + duration_unit: Optional[str] = SchemaField( + description="Task duration unit (minute/day)", default=None, advanced=True + ) + duration: Optional[int] = SchemaField( + description="Task duration amount, You need to selecct the duration unit first", + depends_on=["duration_unit"], + default=None, + advanced=True, + ) + + class Output(BlockSchema): + id: str = SchemaField(description="Task ID") + url: str = SchemaField(description="Task URL") + complete_data: dict = SchemaField( + description="Complete task data as dictionary" + ) + error: str = SchemaField(description="Error message if request failed") + + def __init__(self): + super().__init__( + id="fde4f458-de14-11ef-bf0c-32d3674e8b7e", + description="Creates a new task in a Todoist project", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistCreateTaskBlock.Input, + output_schema=TodoistCreateTaskBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "content": "Buy groceries", + "project_id": "2203306141", + "priority": 4, + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("id", "2995104339"), + ("url", "https://todoist.com/showTask?id=2995104339"), + ( + "complete_data", + { + "id": "2995104339", + "project_id": "2203306141", + "url": "https://todoist.com/showTask?id=2995104339", + }, + ), + ], + test_mock={ + "create_task": lambda *args, **kwargs: ( + "2995104339", + "https://todoist.com/showTask?id=2995104339", + { + "id": "2995104339", + "project_id": "2203306141", + "url": "https://todoist.com/showTask?id=2995104339", + }, + ) + }, + ) + + @staticmethod + def create_task(credentials: TodoistCredentials, content: str, **kwargs): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + task = api.add_task(content=content, **kwargs) + task_dict = Task.to_dict(task) + return task.id, task.url, task_dict + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + due_date = ( + input_data.due_date.strftime("%Y-%m-%d") + if input_data.due_date + else None + ) + deadline_date = ( + input_data.deadline_date.strftime("%Y-%m-%d") + if input_data.deadline_date + else None + ) + + task_args = { + "description": input_data.description, + "project_id": input_data.project_id, + "section_id": input_data.section_id, + "parent_id": input_data.parent_id, + "order": input_data.order, + "labels": input_data.labels, + "priority": input_data.priority, + "due_date": due_date, + "deadline_date": deadline_date, + "assignee_id": input_data.assignee_id, + "duration": input_data.duration, + "duration_unit": input_data.duration_unit, + } + + id, url, complete_data = self.create_task( + credentials, + input_data.content, + **{k: v for k, v in task_args.items() if v is not None}, + ) + + yield "id", id + yield "url", url + yield "complete_data", complete_data + + except Exception as e: + yield "error", str(e) + + +class TodoistGetTasksBlock(Block): + """Get active tasks from Todoist""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + project_id: Optional[str] = SchemaField( + description="Filter tasks by project ID", default=None, advanced=False + ) + section_id: Optional[str] = SchemaField( + description="Filter tasks by section ID", default=None, advanced=True + ) + label: Optional[str] = SchemaField( + description="Filter tasks by label name", default=None, advanced=True + ) + filter: Optional[str] = SchemaField( + description="Filter by any supported filter, You can see How to use filters or create one of your one here - https://todoist.com/help/articles/introduction-to-filters-V98wIH", + default=None, + advanced=True, + ) + lang: Optional[str] = SchemaField( + description="IETF language tag for filter language", default=None + ) + ids: Optional[list[str]] = SchemaField( + description="List of task IDs to retrieve", default=None, advanced=False + ) + + class Output(BlockSchema): + ids: list[str] = SchemaField(description="Task IDs") + urls: list[str] = SchemaField(description="Task URLs") + complete_data: list[dict] = SchemaField( + description="Complete task data as dictionary" + ) + error: str = SchemaField(description="Error message if request failed") + + def __init__(self): + super().__init__( + id="0b706e86-de15-11ef-a113-32d3674e8b7e", + description="Get active tasks from Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistGetTasksBlock.Input, + output_schema=TodoistGetTasksBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "project_id": "2203306141", + }, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("ids", ["2995104339"]), + ("urls", ["https://todoist.com/showTask?id=2995104339"]), + ( + "complete_data", + [ + { + "id": "2995104339", + "project_id": "2203306141", + "url": "https://todoist.com/showTask?id=2995104339", + "is_completed": False, + } + ], + ), + ], + test_mock={ + "get_tasks": lambda *args, **kwargs: [ + { + "id": "2995104339", + "project_id": "2203306141", + "url": "https://todoist.com/showTask?id=2995104339", + "is_completed": False, + } + ] + }, + ) + + @staticmethod + def get_tasks(credentials: TodoistCredentials, **kwargs): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + tasks = api.get_tasks(**kwargs) + return [Task.to_dict(task) for task in tasks] + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + task_filters = { + "project_id": input_data.project_id, + "section_id": input_data.section_id, + "label": input_data.label, + "filter": input_data.filter, + "lang": input_data.lang, + "ids": input_data.ids, + } + + tasks = self.get_tasks( + credentials, **{k: v for k, v in task_filters.items() if v is not None} + ) + + yield "ids", [task["id"] for task in tasks] + yield "urls", [task["url"] for task in tasks] + yield "complete_data", tasks + + except Exception as e: + yield "error", str(e) + + +class TodoistGetTaskBlock(Block): + """Get an active task from Todoist""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + task_id: str = SchemaField(description="Task ID to retrieve") + + class Output(BlockSchema): + project_id: str = SchemaField(description="Project ID containing the task") + url: str = SchemaField(description="Task URL") + complete_data: dict = SchemaField( + description="Complete task data as dictionary" + ) + error: str = SchemaField(description="Error message if request failed") + + def __init__(self): + super().__init__( + id="16d7dc8c-de15-11ef-8ace-32d3674e8b7e", + description="Get an active task from Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistGetTaskBlock.Input, + output_schema=TodoistGetTaskBlock.Output, + test_input={"credentials": TEST_CREDENTIALS_INPUT, "task_id": "2995104339"}, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("project_id", "2203306141"), + ("url", "https://todoist.com/showTask?id=2995104339"), + ( + "complete_data", + { + "id": "2995104339", + "project_id": "2203306141", + "url": "https://todoist.com/showTask?id=2995104339", + }, + ), + ], + test_mock={ + "get_task": lambda *args, **kwargs: { + "project_id": "2203306141", + "id": "2995104339", + "url": "https://todoist.com/showTask?id=2995104339", + } + }, + ) + + @staticmethod + def get_task(credentials: TodoistCredentials, task_id: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + task = api.get_task(task_id=task_id) + return Task.to_dict(task) + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + task_data = self.get_task(credentials, input_data.task_id) + + if task_data: + yield "project_id", task_data["project_id"] + yield "url", task_data["url"] + yield "complete_data", task_data + + except Exception as e: + yield "error", str(e) + + +class TodoistUpdateTaskBlock(Block): + """Updates an existing task in Todoist""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + task_id: str = SchemaField(description="Task ID to update") + content: str = SchemaField(description="Task content", advanced=False) + description: Optional[str] = SchemaField( + description="Task description", default=None, advanced=False + ) + project_id: Optional[str] = SchemaField( + description="Project ID this task should belong to", + default=None, + advanced=False, + ) + section_id: Optional[str] = SchemaField( + description="Section ID this task should belong to", + default=None, + advanced=False, + ) + parent_id: Optional[str] = SchemaField( + description="Parent task ID", default=None, advanced=True + ) + order: Optional[int] = SchemaField( + description="Optional order among other tasks,[Non-zero integer value used by clients to sort tasks under the same parent]", + default=None, + advanced=True, + ) + labels: Optional[list[str]] = SchemaField( + description="Task labels", default=None, advanced=True + ) + priority: Optional[int] = SchemaField( + description="Task priority from 1 (normal) to 4 (urgent)", + default=None, + advanced=True, + ) + due_date: Optional[datetime] = SchemaField( + description="Due date in YYYY-MM-DD format", advanced=True, default=None + ) + deadline_date: Optional[datetime] = SchemaField( + description="Specific date in YYYY-MM-DD format relative to user's timezone", + default=None, + advanced=True, + ) + assignee_id: Optional[str] = SchemaField( + description="Responsible user ID", default=None, advanced=True + ) + duration_unit: Optional[str] = SchemaField( + description="Task duration unit (minute/day)", default=None, advanced=True + ) + duration: Optional[int] = SchemaField( + description="Task duration amount, You need to selecct the duration unit first", + depends_on=["duration_unit"], + default=None, + advanced=True, + ) + + class Output(BlockSchema): + success: bool = SchemaField(description="Whether the update was successful") + error: str = SchemaField(description="Error message if request failed") + + def __init__(self): + super().__init__( + id="1eee6d32-de15-11ef-a2ff-32d3674e8b7e", + description="Updates an existing task in Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistUpdateTaskBlock.Input, + output_schema=TodoistUpdateTaskBlock.Output, + test_input={ + "credentials": TEST_CREDENTIALS_INPUT, + "task_id": "2995104339", + "content": "Buy Coffee", + }, + test_credentials=TEST_CREDENTIALS, + test_output=[("success", True)], + test_mock={"update_task": lambda *args, **kwargs: True}, + ) + + @staticmethod + def update_task(credentials: TodoistCredentials, task_id: str, **kwargs): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + is_success = api.update_task(task_id=task_id, **kwargs) + return is_success + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + due_date = ( + input_data.due_date.strftime("%Y-%m-%d") + if input_data.due_date + else None + ) + deadline_date = ( + input_data.deadline_date.strftime("%Y-%m-%d") + if input_data.deadline_date + else None + ) + + task_updates = {} + if input_data.content is not None: + task_updates["content"] = input_data.content + if input_data.description is not None: + task_updates["description"] = input_data.description + if input_data.project_id is not None: + task_updates["project_id"] = input_data.project_id + if input_data.section_id is not None: + task_updates["section_id"] = input_data.section_id + if input_data.parent_id is not None: + task_updates["parent_id"] = input_data.parent_id + if input_data.order is not None: + task_updates["order"] = input_data.order + if input_data.labels is not None: + task_updates["labels"] = input_data.labels + if input_data.priority is not None: + task_updates["priority"] = input_data.priority + if due_date is not None: + task_updates["due_date"] = due_date + if deadline_date is not None: + task_updates["deadline_date"] = deadline_date + if input_data.assignee_id is not None: + task_updates["assignee_id"] = input_data.assignee_id + if input_data.duration is not None: + task_updates["duration"] = input_data.duration + if input_data.duration_unit is not None: + task_updates["duration_unit"] = input_data.duration_unit + + self.update_task( + credentials, + input_data.task_id, + **{k: v for k, v in task_updates.items() if v is not None}, + ) + + yield "success", True + + except Exception as e: + yield "error", str(e) + + +class TodoistCloseTaskBlock(Block): + """Closes a task in Todoist""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + task_id: str = SchemaField(description="Task ID to close") + + class Output(BlockSchema): + success: bool = SchemaField( + description="Whether the task was successfully closed" + ) + error: str = SchemaField(description="Error message if request failed") + + def __init__(self): + super().__init__( + id="29fac798-de15-11ef-b839-32d3674e8b7e", + description="Closes a task in Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistCloseTaskBlock.Input, + output_schema=TodoistCloseTaskBlock.Output, + test_input={"credentials": TEST_CREDENTIALS_INPUT, "task_id": "2995104339"}, + test_credentials=TEST_CREDENTIALS, + test_output=[("success", True)], + test_mock={"close_task": lambda *args, **kwargs: True}, + ) + + @staticmethod + def close_task(credentials: TodoistCredentials, task_id: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + is_success = api.close_task(task_id=task_id) + return is_success + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + is_success = self.close_task(credentials, input_data.task_id) + yield "success", is_success + + except Exception as e: + yield "error", str(e) + + +class TodoistReopenTaskBlock(Block): + """Reopens a task in Todoist""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + task_id: str = SchemaField(description="Task ID to reopen") + + class Output(BlockSchema): + success: bool = SchemaField( + description="Whether the task was successfully reopened" + ) + error: str = SchemaField(description="Error message if request failed") + + def __init__(self): + super().__init__( + id="2e6bf6f8-de15-11ef-ae7c-32d3674e8b7e", + description="Reopens a task in Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistReopenTaskBlock.Input, + output_schema=TodoistReopenTaskBlock.Output, + test_input={"credentials": TEST_CREDENTIALS_INPUT, "task_id": "2995104339"}, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("success", True), + ], + test_mock={"reopen_task": lambda *args, **kwargs: (True)}, + ) + + @staticmethod + def reopen_task(credentials: TodoistCredentials, task_id: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + is_success = api.reopen_task(task_id=task_id) + return is_success + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + is_success = self.reopen_task(credentials, input_data.task_id) + yield "success", is_success + + except Exception as e: + yield "error", str(e) + + +class TodoistDeleteTaskBlock(Block): + """Deletes a task in Todoist""" + + class Input(BlockSchema): + credentials: TodoistCredentialsInput = TodoistCredentialsField([]) + task_id: str = SchemaField(description="Task ID to delete") + + class Output(BlockSchema): + success: bool = SchemaField( + description="Whether the task was successfully deleted" + ) + error: str = SchemaField(description="Error message if request failed") + + def __init__(self): + super().__init__( + id="33c29ada-de15-11ef-bcbb-32d3674e8b7e", + description="Deletes a task in Todoist", + categories={BlockCategory.PRODUCTIVITY}, + input_schema=TodoistDeleteTaskBlock.Input, + output_schema=TodoistDeleteTaskBlock.Output, + test_input={"credentials": TEST_CREDENTIALS_INPUT, "task_id": "2995104339"}, + test_credentials=TEST_CREDENTIALS, + test_output=[ + ("success", True), + ], + test_mock={"delete_task": lambda *args, **kwargs: (True)}, + ) + + @staticmethod + def delete_task(credentials: TodoistCredentials, task_id: str): + try: + api = TodoistAPI(credentials.access_token.get_secret_value()) + is_success = api.delete_task(task_id=task_id) + return is_success + except Exception as e: + raise e + + def run( + self, + input_data: Input, + *, + credentials: TodoistCredentials, + **kwargs, + ) -> BlockOutput: + try: + is_success = self.delete_task(credentials, input_data.task_id) + yield "success", is_success + + except Exception as e: + yield "error", str(e) diff --git a/autogpt_platform/backend/backend/blocks/twitter/tweets/manage.py b/autogpt_platform/backend/backend/blocks/twitter/tweets/manage.py index 7c86c0abcd..0894b339f5 100644 --- a/autogpt_platform/backend/backend/blocks/twitter/tweets/manage.py +++ b/autogpt_platform/backend/backend/blocks/twitter/tweets/manage.py @@ -92,7 +92,8 @@ class TwitterPostTweetBlock(Block): attachment: Union[Media, DeepLink, Poll, Place, Quote] | None = SchemaField( discriminator="discriminator", description="Additional tweet data (media, deep link, poll, place or quote)", - advanced=True, + advanced=False, + default=Media(discriminator="media"), ) exclude_reply_user_ids: Optional[List[str]] = SchemaField( diff --git a/autogpt_platform/backend/backend/integrations/oauth/__init__.py b/autogpt_platform/backend/backend/integrations/oauth/__init__.py index 50f8a155a6..f64bc2574c 100644 --- a/autogpt_platform/backend/backend/integrations/oauth/__init__.py +++ b/autogpt_platform/backend/backend/integrations/oauth/__init__.py @@ -1,5 +1,7 @@ from typing import TYPE_CHECKING +from backend.integrations.oauth.todoist import TodoistOAuthHandler + from .github import GitHubOAuthHandler from .google import GoogleOAuthHandler from .linear import LinearOAuthHandler @@ -19,6 +21,7 @@ HANDLERS_BY_NAME: dict["ProviderName", type["BaseOAuthHandler"]] = { NotionOAuthHandler, TwitterOAuthHandler, LinearOAuthHandler, + TodoistOAuthHandler, ] } # --8<-- [end:HANDLERS_BY_NAMEExample] diff --git a/autogpt_platform/backend/backend/integrations/oauth/todoist.py b/autogpt_platform/backend/backend/integrations/oauth/todoist.py new file mode 100644 index 0000000000..543a7de84b --- /dev/null +++ b/autogpt_platform/backend/backend/integrations/oauth/todoist.py @@ -0,0 +1,81 @@ +import urllib.parse +from typing import ClassVar, Optional + +import requests + +from backend.data.model import OAuth2Credentials, ProviderName +from backend.integrations.oauth.base import BaseOAuthHandler + + +class TodoistOAuthHandler(BaseOAuthHandler): + PROVIDER_NAME = ProviderName.TODOIST + DEFAULT_SCOPES: ClassVar[list[str]] = [ + "task:add", + "data:read", + "data:read_write", + "data:delete", + "project:delete", + ] + + AUTHORIZE_URL = "https://todoist.com/oauth/authorize" + TOKEN_URL = "https://todoist.com/oauth/access_token" + + def __init__(self, client_id: str, client_secret: str, redirect_uri: str): + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + + def get_login_url( + self, scopes: list[str], state: str, code_challenge: Optional[str] + ) -> str: + params = { + "client_id": self.client_id, + "scope": ",".join(self.DEFAULT_SCOPES), + "state": state, + } + + return f"{self.AUTHORIZE_URL}?{urllib.parse.urlencode(params)}" + + def exchange_code_for_tokens( + self, code: str, scopes: list[str], code_verifier: Optional[str] + ) -> OAuth2Credentials: + """Exchange authorization code for access tokens""" + + data = { + "client_id": self.client_id, + "client_secret": self.client_secret, + "code": code, + "redirect_uri": self.redirect_uri, + } + + response = requests.post(self.TOKEN_URL, data=data) + response.raise_for_status() + + tokens = response.json() + + response = requests.post( + "https://api.todoist.com/sync/v9/sync", + headers={"Authorization": f"Bearer {tokens['access_token']}"}, + data={"sync_token": "*", "resource_types": '["user"]'}, + ) + response.raise_for_status() + user_info = response.json() + user_email = user_info["user"].get("email") + + return OAuth2Credentials( + provider=self.PROVIDER_NAME, + title=None, + username=user_email, + access_token=tokens["access_token"], + refresh_token=None, + access_token_expires_at=None, + refresh_token_expires_at=None, + scopes=scopes, + ) + + def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials: + # Todoist does not support token refresh + return credentials + + def revoke_tokens(self, credentials: OAuth2Credentials) -> bool: + return False diff --git a/autogpt_platform/backend/backend/integrations/providers.py b/autogpt_platform/backend/backend/integrations/providers.py index a9f810fbc9..4898db13a4 100644 --- a/autogpt_platform/backend/backend/integrations/providers.py +++ b/autogpt_platform/backend/backend/integrations/providers.py @@ -33,5 +33,6 @@ class ProviderName(str, Enum): SLANT3D = "slant3d" SMTP = "smtp" TWITTER = "twitter" + TODOIST = "todoist" UNREAL_SPEECH = "unreal_speech" # --8<-- [end:ProviderName] diff --git a/autogpt_platform/backend/backend/util/settings.py b/autogpt_platform/backend/backend/util/settings.py index 153700dbcd..698703dea8 100644 --- a/autogpt_platform/backend/backend/util/settings.py +++ b/autogpt_platform/backend/backend/util/settings.py @@ -320,6 +320,9 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings): linear_client_id: str = Field(default="", description="Linear client ID") linear_client_secret: str = Field(default="", description="Linear client secret") + todoist_client_id: str = Field(default="", description="Todoist client ID") + todoist_client_secret: str = Field(default="", description="Todoist client secret") + stripe_api_key: str = Field(default="", description="Stripe API Key") stripe_webhook_secret: str = Field(default="", description="Stripe Webhook Secret") diff --git a/autogpt_platform/backend/poetry.lock b/autogpt_platform/backend/poetry.lock index deb4bc8fc6..2a432b529c 100644 --- a/autogpt_platform/backend/poetry.lock +++ b/autogpt_platform/backend/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. [[package]] name = "aio-pika" @@ -4296,6 +4296,21 @@ files = [ doc = ["reno", "sphinx"] test = ["pytest", "tornado (>=4.5)", "typeguard"] +[[package]] +name = "todoist-api-python" +version = "2.1.7" +description = "Official Python SDK for the Todoist REST API." +optional = false +python-versions = "<4.0,>=3.9" +groups = ["main"] +files = [ + {file = "todoist_api_python-2.1.7-py3-none-any.whl", hash = "sha256:278bfe851b9bd19bde5ff5de09d813d671ef7310ba55e1962131fca5b59bb735"}, + {file = "todoist_api_python-2.1.7.tar.gz", hash = "sha256:84934a19ccd83fb61010a8126362a5d7d6486c92454c111307ba55bc74903f5c"}, +] + +[package.dependencies] +requests = ">=2.32.3,<3.0.0" + [[package]] name = "tomli" version = "2.2.1" @@ -5035,4 +5050,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.13" -content-hash = "7d37edf4fd4e1935811629fef29d463fa61f904d520aa497de2969fadeba92ec" +content-hash = "9a40ee57d7440f352b0ac0a138fa26d05cd8eadb0cf7d3a0f995fb3c36a2fa7c" diff --git a/autogpt_platform/backend/pyproject.toml b/autogpt_platform/backend/pyproject.toml index 75305a5abd..57c46d1ed4 100644 --- a/autogpt_platform/backend/pyproject.toml +++ b/autogpt_platform/backend/pyproject.toml @@ -55,7 +55,8 @@ sqlalchemy = "^2.0.36" psycopg2-binary = "^2.9.10" google-cloud-storage = "^2.18.2" launchdarkly-server-sdk = "^9.8.0" -mem0ai = "^0.1.48" +mem0ai = "^0.1.44" +todoist-api-python = "^2.1.7" moviepy = "^2.1.2" [tool.poetry.group.dev.dependencies] diff --git a/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx b/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx index 1fd21fcc58..950b2fb25a 100644 --- a/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx +++ b/autogpt_platform/frontend/src/components/integrations/credentials-input.tsx @@ -85,6 +85,7 @@ export const providerIcons: Record< unreal_speech: fallbackIcon, exa: fallbackIcon, hubspot: FaHubspot, + todoist: fallbackIcon, }; // --8<-- [end:ProviderIconsEmbed] diff --git a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx index 8fdf0932b3..0caec6eeab 100644 --- a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx +++ b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx @@ -46,6 +46,7 @@ const providerDisplayNames: Record = { replicate: "Replicate", revid: "Rev.ID", twitter: "Twitter", + todoist: "Todoist", unreal_speech: "Unreal Speech", } as const; // --8<-- [end:CredentialsProviderNames] diff --git a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts index a660c2f1d5..2a813ad0d9 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/types.ts @@ -141,6 +141,7 @@ export const PROVIDER_NAMES = { REDDIT: "reddit", REVID: "revid", UNREAL_SPEECH: "unreal_speech", + TODOIST: "todoist", } as const; // --8<-- [end:BlockIOCredentialsSubSchema] diff --git a/docs/content/platform/blocks/blocks.md b/docs/content/platform/blocks/blocks.md index cfdecf6e67..3ca02d88d3 100644 --- a/docs/content/platform/blocks/blocks.md +++ b/docs/content/platform/blocks/blocks.md @@ -134,7 +134,7 @@ Below is a comprehensive list of all available blocks, categorized by their prim |------------|-------------| | [Twitter Post Tweet](twitter/twitter.md#twitter-post-tweet-block) | Creates a tweet on Twitter with text content and optional attachments including media, polls, quotes, or deep links | | [Twitter Delete Tweet](twitter/twitter.md#twitter-delete-tweet-block) | Deletes a specified tweet using its tweet ID | -| [Twitter Search Recent](twitter/twitter.md#twitter-search-recent-block) | Searches for tweets matching specified criteria with options for filtering and pagination | +| [Twitter Search Recent Tweets](twitter/twitter.md#twitter-search-recent-tweets-block) | Searches for tweets matching specified criteria with options for filtering and pagination | | [Twitter Get Quote Tweets](twitter/twitter.md#twitter-get-quote-tweets-block) | Gets tweets that quote a specified tweet ID with options for pagination and filtering | | [Twitter Retweet](twitter/twitter.md#twitter-retweet-block) | Creates a retweet of a specified tweet using its tweet ID | | [Twitter Remove Retweet](twitter/twitter.md#twitter-remove-retweet-block) | Removes an existing retweet of a specified tweet | @@ -167,4 +167,31 @@ Below is a comprehensive list of all available blocks, categorized by their prim | Twitter Send Direct Message | Working... Sends a direct message to a specified user | | Twitter Create DM Conversation | Working... Creates a new direct message conversation | +## Todoist Integration +| Block Name | Description | +|------------|-------------| +| [Todoist Create Label](todoist.md#todoist-create-label) | Creates a new label in Todoist | +| [Todoist List Labels](todoist.md#todoist-list-labels) | Retrieves all personal labels from Todoist | +| [Todoist Get Label](todoist.md#todoist-get-label) | Retrieves a specific label by ID | +| [Todoist Create Task](todoist.md#todoist-create-task) | Creates a new task in Todoist | +| [Todoist Get Tasks](todoist.md#todoist-get-tasks) | Retrieves active tasks from Todoist | +| [Todoist Update Task](todoist.md#todoist-update-task) | Updates an existing task | +| [Todoist Close Task](todoist.md#todoist-close-task) | Completes/closes a task | +| [Todoist Reopen Task](todoist.md#todoist-reopen-task) | Reopens a completed task | +| [Todoist Delete Task](todoist.md#todoist-delete-task) | Permanently deletes a task | +| [Todoist List Projects](todoist.md#todoist-list-projects) | Retrieves all projects from Todoist | +| [Todoist Create Project](todoist.md#todoist-create-project) | Creates a new project in Todoist | +| [Todoist Get Project](todoist.md#todoist-get-project) | Retrieves details for a specific project | +| [Todoist Update Project](todoist.md#todoist-update-project) | Updates an existing project | +| [Todoist Delete Project](todoist.md#todoist-delete-project) | Deletes a project and its contents | +| [Todoist List Collaborators](todoist.md#todoist-list-collaborators) | Retrieves collaborators on a project | +| [Todoist List Sections](todoist.md#todoist-list-sections) | Retrieves sections from Todoist | +| [Todoist Get Section](todoist.md#todoist-get-section) | Retrieves details for a specific section | +| [Todoist Delete Section](todoist.md#todoist-delete-section) | Deletes a section and its tasks | +| [Todoist Create Comment](todoist.md#todoist-create-comment) | Creates a new comment on a task or project | +| [Todoist Get Comments](todoist.md#todoist-get-comments) | Retrieves all comments for a task or project | +| [Todoist Get Comment](todoist.md#todoist-get-comment) | Retrieves a specific comment by ID | +| [Todoist Update Comment](todoist.md#todoist-update-comment) | Updates an existing comment | +| [Todoist Delete Comment](todoist.md#todoist-delete-comment) | Deletes a comment | + This comprehensive list covers all the blocks available in AutoGPT. Each block is designed to perform a specific task, and they can be combined to create powerful, automated workflows. For more detailed information on each block, click on its name to view the full documentation. diff --git a/docs/content/platform/blocks/todoist.md b/docs/content/platform/blocks/todoist.md new file mode 100644 index 0000000000..65195f0b81 --- /dev/null +++ b/docs/content/platform/blocks/todoist.md @@ -0,0 +1,722 @@ +# Todoist Blocks + +## Todoist Create Label + +### What it is +A block that creates a new label in Todoist. + +### What it does +Creates a new label in Todoist with specified name, order, color and favorite status. + +### How it works +It takes label details as input, connects to Todoist API, creates the label and returns the created label's details. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Name | Name of the label | +| Order | Optional label order | +| Color | Optional color of the label icon | +| Is Favorite | Whether label is marked as favorite | + +### Outputs +| Output | Description | +|--------|-------------| +| ID | ID of the created label | +| Name | Name of the label | +| Color | Color of the label | +| Order | Label order | +| Is Favorite | Favorite status | +| Error | Error message if request failed | + +### Possible use case +Creating new labels to organize and categorize tasks in Todoist. + +--- + +## Todoist List Labels + +### What it is +A block that retrieves all personal labels from Todoist. + +### What it does +Fetches all personal labels from the user's Todoist account. + +### How it works +Connects to Todoist API using provided credentials and retrieves all labels. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | + +### Outputs +| Output | Description | +|--------|-------------| +| Labels | List of complete label data | +| Label IDs | List of label IDs | +| Label Names | List of label names | +| Error | Error message if request failed | + +### Possible use case +Getting an overview of all labels to organize tasks or find specific labels. + +--- + +## Todoist Get Label + +### What it is +A block that retrieves a specific label by ID. + +### What it does +Fetches details of a specific label using its ID. + +### How it works +Uses the label ID to retrieve label details from Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Label ID | ID of label to retrieve | + +### Outputs +| Output | Description | +|--------|-------------| +| ID | Label ID | +| Name | Label name | +| Color | Label color | +| Order | Label order | +| Is Favorite | Favorite status | +| Error | Error message if request failed | + +### Possible use case +Looking up details of a specific label for editing or verification. + +--- + +## Todoist Create Task + +### What it is +A block that creates a new task in Todoist. + +### What it does +Creates a new task with specified content, description, project assignment and other optional parameters. + +### How it works +Takes task details and creates a new task via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Content | Task content | +| Description | Optional task description | +| Project ID | Optional project to add task to | +| Section ID | Optional section to add task to | +| Parent ID | Optional parent task ID | +| Order | Optional task order | +| Labels | Optional task labels | +| Priority | Optional priority (1-4) | +| Due Date | Optional due date | +| Deadline Date | Optional deadline date | +| Assignee ID | Optional assignee | +| Duration Unit | Optional duration unit | +| Duration | Optional duration amount | + +### Outputs +| Output | Description | +|--------|-------------| +| ID | Created task ID | +| URL | Task URL | +| Complete Data | Complete task data | +| Error | Error message if request failed | + +### Possible use case +Creating new tasks with full customization of parameters. + +--- + +## Todoist Get Tasks + +### What it is +A block that retrieves active tasks from Todoist. + +### What it does +Fetches tasks based on optional filters like project, section, label etc. + +### How it works +Queries Todoist API with provided filters to get matching tasks. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Project ID | Optional filter by project | +| Section ID | Optional filter by section | +| Label | Optional filter by label | +| Filter | Optional custom filter string | +| Lang | Optional filter language | +| IDs | Optional specific task IDs | + +### Outputs +| Output | Description | +|--------|-------------| +| IDs | List of task IDs | +| URLs | List of task URLs | +| Complete Data | Complete task data | +| Error | Error message if request failed | + +### Possible use case +Retrieving tasks matching specific criteria for review or processing. + +--- + +## Todoist Update Task + +### What it is +A block that updates an existing task. + +### What it does +Updates specified fields of an existing task. + +### How it works +Takes task ID and updated fields, applies changes via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Task ID | ID of task to update | +| Content | New task content | +| Description | New description | +| Project ID | New project ID | +| Section ID | New section ID | +| Parent ID | New parent task ID | +| Order | New order | +| Labels | New labels | +| Priority | New priority | +| Due Date | New due date | +| Deadline Date | New deadline date | +| Assignee ID | New assignee | +| Duration Unit | New duration unit | +| Duration | New duration | + +### Outputs +| Output | Description | +|--------|-------------| +| Success | Whether update succeeded | +| Error | Error message if failed | + +### Possible use case +Modifying task details like due dates, priority etc. + +--- + +## Todoist Close Task + +### What it is +A block that completes/closes a task. + +### What it does +Marks a task as complete in Todoist. + +### How it works +Uses task ID to mark it complete via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Task ID | ID of task to close | + +### Outputs +| Output | Description | +|--------|-------------| +| Success | Whether task was closed | +| Error | Error message if failed | + +### Possible use case +Marking tasks as done in automated workflows. + +--- + +## Todoist Reopen Task + +### What it is +A block that reopens a completed task. + +### What it does +Marks a completed task as active again. + +### How it works +Uses task ID to reactivate via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Task ID | ID of task to reopen | + +### Outputs +| Output | Description | +|--------|-------------| +| Success | Whether task was reopened | +| Error | Error message if failed | + +### Possible use case +Reactivating tasks that were closed accidentally or need to be repeated. + +--- + +## Todoist Delete Task + +### What it is +A block that permanently deletes a task. + +### What it does +Removes a task completely from Todoist. + +### How it works +Uses task ID to delete via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Task ID | ID of task to delete | + +### Outputs +| Output | Description | +|--------|-------------| +| Success | Whether deletion succeeded | +| Error | Error message if failed | + +### Possible use case +Removing unwanted or obsolete tasks from the system. + +--- + +## Todoist List Projects + +### What it is +A block that retrieves all projects from Todoist. + +### What it does +Fetches all projects and their details from a user's Todoist account. + +### How it works +Connects to Todoist API using provided credentials and retrieves all projects. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | + +### Outputs +| Output | Description | +|--------|-------------| +| Names List | List of project names | +| IDs List | List of project IDs | +| URL List | List of project URLs | +| Complete Data | Complete project data | +| Error | Error message if request failed | + +### Possible use case +Getting an overview of all projects for organization or automation. + +--- + +## Todoist Create Project + +### What it is +A block that creates a new project in Todoist. + +### What it does +Creates a new project with specified name, parent project, color and other settings. + +### How it works +Takes project details and creates via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Name | Name of the project | +| Parent ID | Optional parent project ID | +| Color | Optional color of project icon | +| Is Favorite | Whether project is favorite | +| View Style | Display style (list/board) | + +### Outputs +| Output | Description | +|--------|-------------| +| Success | Whether creation succeeded | +| Error | Error message if failed | + +### Possible use case +Creating new projects programmatically for workflow automation. + +--- + +## Todoist Get Project + +### What it is +A block that retrieves details for a specific project. + +### What it does +Fetches complete details of a single project by ID. + +### How it works +Uses project ID to retrieve details via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Project ID | ID of project to get | + +### Outputs +| Output | Description | +|--------|-------------| +| Project ID | ID of the project | +| Project Name | Name of the project | +| Project URL | URL of the project | +| Complete Data | Complete project data | +| Error | Error message if failed | + +### Possible use case +Looking up project details for verification or editing. + +--- + +## Todoist Update Project + +### What it is +A block that updates an existing project. + +### What it does +Updates specified fields of an existing project. + +### How it works +Takes project ID and updated fields, applies via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Project ID | ID of project to update | +| Name | New project name | +| Color | New color for icon | +| Is Favorite | New favorite status | +| View Style | New display style | + +### Outputs +| Output | Description | +|--------|-------------| +| Success | Whether update succeeded | +| Error | Error message if failed | + +### Possible use case +Modifying project settings or reorganizing projects. + +--- + +## Todoist Delete Project + +### What it is +A block that deletes a project and its contents. + +### What it does +Permanently removes a project including sections and tasks. + +### How it works +Uses project ID to delete via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Project ID | ID of project to delete | + +### Outputs +| Output | Description | +|--------|-------------| +| Success | Whether deletion succeeded | +| Error | Error message if failed | + +### Possible use case +Removing completed or obsolete projects. + +--- + +## Todoist List Collaborators + +### What it is +A block that retrieves collaborators on a project. + +### What it does +Fetches all collaborators and their details for a specific project. + +### How it works +Uses project ID to get collaborator list via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Project ID | ID of project to check | + +### Outputs +| Output | Description | +|--------|-------------| +| Collaborator IDs | List of collaborator IDs | +| Collaborator Names | List of collaborator names | +| Collaborator Emails | List of collaborator emails | +| Complete Data | Complete collaborator data | +| Error | Error message if failed | + +### Possible use case +Managing project sharing and collaboration. + +--- + +## Todoist List Sections + +### What it is +A block that retrieves sections from Todoist. + +### What it does +Fetches all sections, optionally filtered by project. + +### How it works +Connects to Todoist API to retrieve sections list. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Project ID | Optional project filter | + +### Outputs +| Output | Description | +|--------|-------------| +| Names List | List of section names | +| IDs List | List of section IDs | +| Complete Data | Complete section data | +| Error | Error message if failed | + +### Possible use case +Getting section information for task organization. + +--- + +## Todoist Get Section + +### What it is +A block that retrieves details for a specific section. + +### What it does +Fetches complete details of a single section by ID. + +### How it works +Uses section ID to retrieve details via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Section ID | ID of section to get | + +### Outputs +| Output | Description | +|--------|-------------| +| ID | Section ID | +| Project ID | Parent project ID | +| Order | Section order | +| Name | Section name | +| Error | Error message if failed | + +### Possible use case +Looking up section details for task management. + +--- + +## Todoist Delete Section + +### What it is +A block that deletes a section and its tasks. + +### What it does +Permanently removes a section including all tasks. + +### How it works +Uses section ID to delete via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Section ID | ID of section to delete | + +### Outputs +| Output | Description | +|--------|-------------| +| Success | Whether deletion succeeded | +| Error | Error message if failed | + +### Possible use case +Removing unused sections or reorganizing projects. + +--- + +## Todoist Create Comment + +### What it is +A block that creates a new comment on a Todoist task or project. + +### What it does +Creates a comment with specified content on either a task or project. + +### How it works +Takes comment content and task/project ID, creates comment via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Content | Comment content | +| ID Type | Task ID or Project ID to comment on | +| Attachment | Optional file attachment | + +### Outputs +| Output | Description | +|--------|-------------| +| ID | ID of created comment | +| Content | Comment content | +| Posted At | Comment timestamp | +| Task ID | Associated task ID | +| Project ID | Associated project ID | +| Error | Error message if request failed | + +### Possible use case +Adding notes and comments to tasks or projects automatically. + +--- + +## Todoist Get Comments + +### What it is +A block that retrieves all comments for a task or project. + +### What it does +Fetches all comments associated with a specific task or project. + +### How it works +Uses task/project ID to get comments list via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| ID Type | Task ID or Project ID to get comments for | + +### Outputs +| Output | Description | +|--------|-------------| +| Comments | List of comments | +| Error | Error message if request failed | + +### Possible use case +Reviewing comment history on tasks or projects. + +--- + +## Todoist Get Comment + +### What it is +A block that retrieves a specific comment by ID. + +### What it does +Fetches details of a single comment using its ID. + +### How it works +Uses comment ID to retrieve details via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Comment ID | ID of comment to retrieve | + +### Outputs +| Output | Description | +|--------|-------------| +| Content | Comment content | +| ID | Comment ID | +| Posted At | Comment timestamp | +| Project ID | Associated project ID | +| Task ID | Associated task ID | +| Attachment | Optional file attachment | +| Error | Error message if request failed | + +### Possible use case +Looking up specific comment details for reference. + +--- + +## Todoist Update Comment + +### What it is +A block that updates an existing comment. + +### What it does +Updates the content of a specific comment. + +### How it works +Takes comment ID and new content, updates via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Comment ID | ID of comment to update | +| Content | New content for the comment | + +### Outputs +| Output | Description | +|--------|-------------| +| Success | Whether update succeeded | +| Error | Error message if request failed | + +### Possible use case +Modifying existing comments to fix errors or update information. + +--- + +## Todoist Delete Comment + +### What it is +A block that deletes a comment. + +### What it does +Permanently removes a comment from a task or project. + +### How it works +Uses comment ID to delete via Todoist API. + +### Inputs +| Input | Description | +|-------|-------------| +| Credentials | Todoist API credentials | +| Comment ID | ID of comment to delete | + +### Outputs +| Output | Description | +|--------|-------------| +| Success | Whether deletion succeeded | +| Error | Error message if request failed | + +### Possible use case +Removing outdated or incorrect comments from tasks/projects. diff --git a/docs/content/platform/blocks/twitter/twitter.md b/docs/content/platform/blocks/twitter/twitter.md index 3b4e4b00bc..bdf8f2fa21 100644 --- a/docs/content/platform/blocks/twitter/twitter.md +++ b/docs/content/platform/blocks/twitter/twitter.md @@ -122,22 +122,22 @@ It uses the Twitter API (Tweepy) to fetch quote tweets for a given tweet ID, han | max_results | Maximum number of results to return (max 100) | | exclude | Types of tweets to exclude | | pagination_token | Token for getting next page of results | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). | -| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). | -| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| media_fields | Media-related fields to include [more info](twitter.md#common-input). | +| place_fields | Location-related fields to include [more info](twitter.md#common-input). | +| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | |--------|-------------| | ids | List of quote tweet IDs | | texts | List of quote tweet text contents | -| next_token | Token for retrieving next page [more info](twitter/twitter.md#common-output). | +| next_token | Token for retrieving next page [more info](twitter.md#common-output). | | data | Complete tweet data | -| included | Additional requested data [more info](twitter/twitter.md#common-output). | -| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). | +| included | Additional requested data [more info](twitter.md#common-output). | +| meta | Pagination and result metadata [more info](twitter.md#common-output). | | error | Error message if request failed | ### Possible use case @@ -219,12 +219,12 @@ It uses the Twitter API (Tweepy) to fetch retweeter information for a given twee | tweet_id | ID of the tweet to get retweeters for | | max_results | Maximum number of results per page (1-100) | | pagination_token | Token for getting next page of results | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). | -| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). | -| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| media_fields | Media-related fields to include [more info](twitter.md#common-input). | +| place_fields | Location-related fields to include [more info](twitter.md#common-input). | +| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -234,8 +234,8 @@ It uses the Twitter API (Tweepy) to fetch retweeter information for a given twee | usernames | List of usernames who retweeted | | next_token | Token for retrieving next page | | data | Complete user data | -| included | Additional requested data [more info](twitter/twitter.md#common-output). | -| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). | +| included | Additional requested data [more info](twitter.md#common-output). | +| meta | Pagination and result metadata [more info](twitter.md#common-output). | | error | Error message if request failed | ### Possible use case @@ -261,12 +261,12 @@ It queries the Twitter API (Tweepy) with the provided user ID to fetch tweets me | user_id | ID of user to get mentions for | | max_results | Number of results per page (5-100) | | pagination_token | Token for getting next page of results | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). | -| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). | -| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| media_fields | Media-related fields to include [more info](twitter.md#common-input). | +| place_fields | Location-related fields to include [more info](twitter.md#common-input). | +| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -277,8 +277,8 @@ It queries the Twitter API (Tweepy) with the provided user ID to fetch tweets me | userNames | List of usernames who mentioned target user | | next_token | Token for retrieving next page | | data | Complete tweet data | -| included | Additional requested data [more info](twitter/twitter.md#common-output). | -| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). | +| included | Additional requested data [more info](twitter.md#common-output). | +| meta | Pagination and result metadata [more info](twitter.md#common-output). | | error | Error message if request failed | ### Possible use case @@ -303,12 +303,12 @@ It uses the Twitter API (Tweepy) to fetch tweets from the home timeline, handlin | credentials | Twitter API credentials with required scopes | | max_results | Number of results per page (5-100) | | pagination_token | Token for getting next page of results | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). | -| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). | -| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| media_fields | Media-related fields to include [more info](twitter.md#common-input). | +| place_fields | Location-related fields to include [more info](twitter.md#common-input). | +| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -319,8 +319,8 @@ It uses the Twitter API (Tweepy) to fetch tweets from the home timeline, handlin | userNames | List of usernames who authored tweets | | next_token | Token for retrieving next page | | data | Complete tweet data | -| included | Additional requested data [more info](twitter/twitter.md#common-output). | -| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). | +| included | Additional requested data [more info](twitter.md#common-output). | +| meta | Pagination and result metadata [more info](twitter.md#common-output). | | error | Error message if request failed | ### Possible use case @@ -346,12 +346,12 @@ It uses the Twitter API (Tweepy) to fetch tweets from a specified user's timelin | user_id | ID of user to get tweets from | | max_results | Number of results per page (5-100) | | pagination_token | Token for getting next page of results | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). | -| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). | -| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| media_fields | Media-related fields to include [more info](twitter.md#common-input). | +| place_fields | Location-related fields to include [more info](twitter.md#common-input). | +| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -362,8 +362,8 @@ It uses the Twitter API (Tweepy) to fetch tweets from a specified user's timelin | userNames | List of usernames who authored tweets | | next_token | Token for retrieving next page | | data | Complete tweet data | -| included | Additional requested data [more info](twitter/twitter.md#common-output). | -| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). | +| included | Additional requested data [more info](twitter.md#common-output). | +| meta | Pagination and result metadata [more info](twitter.md#common-output). | | error | Error message if request failed | ### Possible use case @@ -387,12 +387,12 @@ It uses the Twitter API (Tweepy) to fetch a single tweet by its ID, handling aut |-------|-------------| | credentials | Twitter API credentials with required scopes | | tweet_id | ID of the tweet to fetch | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). | -| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). | -| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| media_fields | Media-related fields to include [more info](twitter.md#common-input). | +| place_fields | Location-related fields to include [more info](twitter.md#common-input). | +| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -402,8 +402,8 @@ It uses the Twitter API (Tweepy) to fetch a single tweet by its ID, handling aut | userId | ID of tweet author | | userName | Username of tweet author | | data | Complete tweet data | -| included | Additional requested data [more info](twitter/twitter.md#common-output). | -| meta | Tweet metadata [more info](twitter/twitter.md#common-output). | +| included | Additional requested data [more info](twitter.md#common-output). | +| meta | Tweet metadata [more info](twitter.md#common-output). | | error | Error message if request failed | ### Possible use case @@ -427,12 +427,12 @@ It uses the Twitter API (Tweepy) to batch fetch tweets by their IDs, handling au |-------|-------------| | credentials | Twitter API credentials with required scopes | | tweet_ids | List of tweet IDs to fetch (max 100) | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). | -| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). | -| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| media_fields | Media-related fields to include [more info](twitter.md#common-input). | +| place_fields | Location-related fields to include [more info](twitter.md#common-input). | +| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -442,8 +442,8 @@ It uses the Twitter API (Tweepy) to batch fetch tweets by their IDs, handling au | userIds | List of tweet author IDs | | userNames | List of tweet author usernames | | data | Complete tweet data array | -| included | Additional requested data [more info](twitter/twitter.md#common-output). | -| meta | Tweet metadata [more info](twitter/twitter.md#common-output). | +| included | Additional requested data [more info](twitter.md#common-output). | +| meta | Tweet metadata [more info](twitter.md#common-output). | | error | Error message if request failed | ### Possible use case @@ -497,9 +497,9 @@ It uses the Twitter API (Tweepy) to fetch information about users who liked a gi | tweet_id | ID of tweet to get liking users for | | max_results | Maximum number of results to return (1-100) | | pagination_token | Token for getting next page of results | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -508,8 +508,8 @@ It uses the Twitter API (Tweepy) to fetch information about users who liked a gi | username | List of usernames who liked | | next_token | Token for retrieving next page | | data | Complete user data | -| included | Additional requested data [more info](twitter/twitter.md#common-output). | -| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). | +| included | Additional requested data [more info](twitter.md#common-output). | +| meta | Pagination and result metadata [more info](twitter.md#common-output). | | error | Error message if request failed | ### Possible use case @@ -535,12 +535,12 @@ It uses the Twitter API (Tweepy) to fetch tweets liked by a given user ID, handl | user_id | ID of user to get liked tweets for | | max_results | Maximum number of results per page (5-100) | | pagination_token | Token for getting next page of results | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). | -| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). | -| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| media_fields | Media-related fields to include [more info](twitter.md#common-input). | +| place_fields | Location-related fields to include [more info](twitter.md#common-input). | +| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -551,8 +551,8 @@ It uses the Twitter API (Tweepy) to fetch tweets liked by a given user ID, handl | userNames | List of tweet author usernames | | next_token | Token for retrieving next page | | data | Complete tweet data | -| included | Additional requested data [more info](twitter/twitter.md#common-output). | -| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). | +| included | Additional requested data [more info](twitter.md#common-output). | +| meta | Pagination and result metadata [more info](twitter.md#common-output). | | error | Error message if request failed | ### Possible use case @@ -689,12 +689,12 @@ It uses the Twitter API (Tweepy) to fetch bookmarked tweets, handling pagination | credentials | Twitter API credentials with required scopes | | max_results | Maximum number of results per page (1-100) | | pagination_token | Token for getting next page of results | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). | -| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). | -| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| media_fields | Media-related fields to include [more info](twitter.md#common-input). | +| place_fields | Location-related fields to include [more info](twitter.md#common-input). | +| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -705,8 +705,8 @@ It uses the Twitter API (Tweepy) to fetch bookmarked tweets, handling pagination | userName | List of tweet author usernames | | next_token | Token for retrieving next page | | data | Complete tweet data | -| included | Additional requested data [more info](twitter/twitter.md#common-output). | -| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). | +| included | Additional requested data [more info](twitter.md#common-output). | +| meta | Pagination and result metadata [more info](twitter.md#common-output). | | error | Error message if request failed | ### Possible use case @@ -906,9 +906,9 @@ It uses the Twitter API (Tweepy) to fetch followers for a given user ID, handlin | target_user_id | ID of user to get followers for | | max_results | Maximum number of results per page (1-1000) | | pagination_token | Token for getting next page of results | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -917,8 +917,8 @@ It uses the Twitter API (Tweepy) to fetch followers for a given user ID, handlin | usernames | List of follower usernames | | next_token | Token for retrieving next page | | data | Complete user data | -| includes | Additional requested data [more info](twitter/twitter.md#common-output). | -| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). | +| includes | Additional requested data [more info](twitter.md#common-output). | +| meta | Pagination and result metadata [more info](twitter.md#common-output). | | error | Error message if request failed | ### Possible use case @@ -944,9 +944,9 @@ It uses the Twitter API (Tweepy) to fetch following list for a given user ID, ha | target_user_id | ID of user to get following list for | | max_results | Maximum number of results per page (1-1000) | | pagination_token | Token for getting next page of results | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -955,8 +955,8 @@ It uses the Twitter API (Tweepy) to fetch following list for a given user ID, ha | usernames | List of following usernames | | next_token | Token for retrieving next page | | data | Complete user data | -| includes | Additional requested data [more info](twitter/twitter.md#common-output). | -| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). | +| includes | Additional requested data [more info](twitter.md#common-output). | +| meta | Pagination and result metadata [more info](twitter.md#common-output). | | error | Error message if request failed | ### Possible use case @@ -1009,9 +1009,9 @@ It uses the Twitter API (Tweepy) to fetch muted users, handling authentication a | credentials | Twitter API credentials with required scopes | | max_results | Maximum results per page (1-1000, default 10) | | pagination_token | Token for getting next/previous page | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -1073,9 +1073,9 @@ It uses the Twitter API (Tweepy) to fetch user data for a single user identified |-------|-------------| | credentials | Twitter API credentials with required scopes | | identifier | User identifier (either user ID or username) | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -1108,9 +1108,9 @@ It uses the Twitter API (Tweepy) to batch fetch user data for multiple users ide |-------|-------------| | credentials | Twitter API credentials with required scopes | | identifier | List of user identifiers (either user IDs or usernames, max 100) | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -1145,9 +1145,9 @@ It uses the Twitter API (Tweepy) to search for Spaces matching the query paramet | query | Search term to find in Space titles | | max_results | Maximum number of results to return (1-100, default 10) | | state | Type of Spaces to return (live, scheduled, or all) | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| space_fields | Space-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| space_fields | Space-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -1182,9 +1182,9 @@ It uses the Twitter API (Tweepy) to batch fetch Space data for multiple Spaces, |-------|-------------| | credentials | Twitter API credentials with required scopes | | identifier | Choice of lookup by Space IDs or creator user IDs | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| space_fields | Space-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| space_fields | Space-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -1216,9 +1216,9 @@ It uses the Twitter API (Tweepy) to fetch Space data for a single Space ID, hand |-------|-------------| | credentials | Twitter API credentials with required scopes | | space_id | ID of Space to retrieve | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| space_fields | Space-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| space_fields | Space-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -1251,8 +1251,8 @@ It uses the Twitter API (Tweepy) to fetch buyer information for a Space, handlin |-------|-------------| | credentials | Twitter API credentials with required scopes | | space_id | ID of Space to get buyers for | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -1284,12 +1284,12 @@ It uses the Twitter API (Tweepy) to fetch tweets from a Space, handling authenti |-------|-------------| | credentials | Twitter API credentials with required scopes | | space_id | ID of Space to get tweets for | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). | -| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). | -| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| media_fields | Media-related fields to include [more info](twitter.md#common-input). | +| place_fields | Location-related fields to include [more info](twitter.md#common-input). | +| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -1322,9 +1322,9 @@ It uses the Twitter API (Tweepy) to fetch list data for a single list ID, handli |-------|-------------| | credentials | Twitter API credentials with required scopes | | list_id | ID of the Twitter List to retrieve | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| list_fields | List-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| list_fields | List-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -1361,9 +1361,9 @@ It uses the Twitter API (Tweepy) to fetch owned lists for a given user ID, handl | user_id | ID of user whose Lists to retrieve | | max_results | Maximum results per page (1-100, default 10) | | pagination_token | Token for getting next page | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| list_fields | List-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| list_fields | List-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -1532,12 +1532,12 @@ It uses the Twitter API (Tweepy) to fetch tweets from a specified List, handling | list_id | ID of the List to get tweets from | | max_results | Maximum number of results per page (1-100, default 10) | | pagination_token | Token for getting next page of results | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| media_fields | Media-related fields to include [more info](twitter/twitter.md#common-input). | -| place_fields | Location-related fields to include [more info](twitter/twitter.md#common-input). | -| poll_fields | Poll-related fields to include [more info](twitter/twitter.md#common-input). | -| tweet_fields | Tweet-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| media_fields | Media-related fields to include [more info](twitter.md#common-input). | +| place_fields | Location-related fields to include [more info](twitter.md#common-input). | +| poll_fields | Poll-related fields to include [more info](twitter.md#common-input). | +| tweet_fields | Tweet-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -1546,8 +1546,8 @@ It uses the Twitter API (Tweepy) to fetch tweets from a specified List, handling | texts | List of tweet text contents | | next_token | Token for retrieving next page | | data | Complete tweet data array | -| included | Additional requested data [more info](twitter/twitter.md#common-output). | -| meta | Pagination and result metadata [more info](twitter/twitter.md#common-output). | +| included | Additional requested data [more info](twitter.md#common-output). | +| meta | Pagination and result metadata [more info](twitter.md#common-output). | | error | Error message if request failed | ### Possible use case @@ -1715,9 +1715,9 @@ It uses the Twitter API (Tweepy) to fetch pinned Lists data, handling authentica | Input | Description | |-------|-------------| | credentials | Twitter API credentials with required scopes | -| expansions | Additional data fields to include [more info](twitter/twitter.md#common-input). | -| list_fields | List-specific fields to include [more info](twitter/twitter.md#common-input). | -| user_fields | User-related fields to include [more info](twitter/twitter.md#common-input). | +| expansions | Additional data fields to include [more info](twitter.md#common-input). | +| list_fields | List-specific fields to include [more info](twitter.md#common-input). | +| user_fields | User-related fields to include [more info](twitter.md#common-input). | ### Outputs | Output | Description | @@ -1905,3 +1905,62 @@ Information about Twitter users - For user data in Tweets, add `expansions=Author_User_ID` and appropriate `user_fields`. - Data returned under `includes` helps cross-reference expanded data objects with their parent entities using IDs. + +## Common Output + +The Twitter API returns standardized response elements across many endpoints. Here are the common output fields you'll encounter: + +### data +The primary data requested in the response + +| Field | Description | +|-------|-------------| +| ID | Unique identifier for the object | +| Type | Type of object (tweet, user, etc) | +| Properties | Object-specific fields like text for tweets | + +### includes +Additional expanded data objects referenced in the primary data + +| Field | Description | +|-------|-------------| +| Tweets | Full tweet objects that were referenced | +| Users | User profile data for authors/mentions | +| Places | Location data for geo-tagged content | +| Media | Details about attached photos/videos | +| Polls | Information about embedded polls | + +### meta +Metadata about the response and pagination + +| Field | Description | +|-------|-------------| +| Result_Count | Number of items returned | +| Next_Token | Token to get next page of results | +| Previous_Token | Token to get previous page | +| Newest_ID | Most recent ID in results | +| Oldest_ID | Oldest ID in results | +| Total_Tweet_Count | Total matching tweets (search) | + +### errors +Details about any errors that occurred + +| Field | Description | +|-------|-------------| +| Title | Brief error description | +| Detail | Detailed error message | +| Type | Error category/classification | +| Status | HTTP status code | + +### Non-paginated responses +For single-object lookups: +- data: Contains requested object +- includes: Referenced objects +- errors: Any errors encountered + +### Paginated responses +For multi-object lookups: +- data: Array of objects +- includes: Referenced objects +- meta: Pagination details +- errors: Any errors encountered