Manage Todoist with a python-powered Telegram bot

Manage Todoist with a python-powered Telegram bot

Written by ahmed.abdulqasim on Oct 9th, 2022 Views Report Post

In this post, I will explain how to create a Telegram bot in Python. Todoist is a great tool to manage to-do lists. Although this post can be just a general tutorial about creating a Telegram bot, I will go through connecting to Todoist API and bringing its power into Telegram, so you can learn more about working with real-world API and interacting with users in Telegram bots.

This post assumes you know basic Python. What you will learn includes:

  • Creating a Telegram Bot with Telegram Bot Father
  • Working with API in Python
  • Writing Telegram bot script in Python
  • Interacting with users in Telegram bot(showing buttons, replying to message, etc.) Creating a Telegram Bot First of all, we need to get a Telegram handle for our bot. Also, we need an access token to connect and interact with Telegram.

In your Telegram app, go to BotFather bot by clicking on this link or writing bot name in the Telegram search bar. In the BotFather, you can create a new bot with /newbot command. Just talk to the Telegram BotFather and it will guide you. First, you need to provide a name for your bot, and then you can create a username for it, in which your bot will be accessible via that username as a link like this: telegram.me/<bot_username> or a Telegram handle like @bot_username

After that, you will get a message with your bot data, including your bot’s access token for HTTP API. We will use this in the next steps.

Interacting with API in Python

To build a useful Telegram bot, You need to have a good knowledge of working with API.

I read a lot of tutorials for creating Telegram bots, but I wanted to write a bot that works with real-world API, not a bot to show some cat pictures(which is also very cool, by the way).

Todoist is a great tool for managing to-dos. Although it has a great mobile application, it may help you or your team to have your tasks in Telegram.

Also using Todoist API, you will learn how to interact with real-world APIs.

APIHandler Class

Todoist has an official Python API library. Using it you can easily interact with Todoist API. First, we are going to examine this official library. Then with the Python requests package, we are interacting with Todoist REST API. Because many of third-party API doesn’ thave such Python libraries, it’s important to learn how to use Python requests package to make HTTP requests for RESTful API.

Getting Todoist access token

Before we begin, you need to get an access token for Todoist API. Simply go to the Todoist app console and create a new app. After creating your app, you will see client id and client secret which you need to use in a production-level app for user authentication.

For now, we only need to get the access token for our tests. Scroll down and you can see your access token. Now let’s try it in action.

Todoist official Python library

Now install TodoistAPI package with the Python pip running this command: pip install todoist-python

Using Todoist API library, it is very easy to get the projects:

from todoist.api import TodoistAPI
api_token = ""
result = TodoistAPI(api_token).sync()
project_list = result.state['projects']

Don’t forget to put your access token in api_token variable. (test token)

Interacting with RESTful API using Python requests

To make an API call you need to use Python requests package. Install it using pip: pip install requests

The requests package has two main functions we can use to make get or post requests.

We can make a get request to get all tasks from Todoist.

import requests

api_url = "https://beta.todoist.com/API/v8"
api_token = ""
requests.get(
 "%s/tasks" % api_url,
 headers={
  "Authorization": "Bearer %s" % api_token
 }).json()

Or, make a post request to create a new task in Todoist

import requests

api_url = "https://beta.todoist.com/API/v8"
api_token = ""
requests.post(
 "%s/tasks" % api_url,
 data=json.dumps({
  "content": task_content,
 }),
 headers={
  "Content-Type": "application/json",
  "X-Request-Id": str(uuid.uuid4()),
  "Authorization": "Bearer %s" % api_token
 }).json()

To have a clean code let’s create a class for our API handler.

from todoist.api import TodoistAPI
import requests
from datetime import datetime
import uuid
import json


class APIHandler:

    def __init__(self, api_token, api_url):
        # initiate a todoist api instance
        self.api = TodoistAPI(api_token)
        self.api_token = api_token
        self.api_url = api_url

    def get_project_list(self):
        self.api.sync()
        project_list = self.api.state['projects']
        return project_list

    def get_tasks_by_project(self, project_id):
        tasks_list = requests.get(
            "%s/tasks" % self.api_url,
            params={
                "project_id": project_id
            },
            headers={
                "Authorization": "Bearer %s" % self.api_token
            }).json()

        return tasks_list

    def create_project(self, project_name):
        self.api.projects.add(project_name)
        self.api.commit()
        return True

    def get_all_tasks(self):
        tasks_list = requests.get(
            "%s/tasks" % self.api_url,
            headers={
                "Authorization": "Bearer %s" % self.api_token
            }).json()
        return tasks_list

    def get_today_tasks(self):
        all_tasks = self.get_all_tasks()
        today_tasks = []

        today = datetime.today().date()
        for task in all_tasks:
            task_due = task.get('due')
            if task_due:
                task_due_date_string = task_due.get('date')
                task_due_date = datetime.strptime(task_due_date_string, '%Y-%m-%d').date()
                if task_due_date == today:
                    today_tasks.append(task)

        return today_tasks

    def create_task(self, task_content):
        result = requests.post(
            "%s/tasks" % self.api_url,
            data=json.dumps({
                "content": task_content,
            }),
            headers={
                "Content-Type": "application/json",
                "X-Request-Id": str(uuid.uuid4()),
                "Authorization": "Bearer %s" % self.api_token
            }).json()

        return result

In __init__ function, we get two parameters to set api_token and api_url. Also, We create an instance of TodoisAPI from Todoist Python library to use when it’s necessary.

All other functions are using requests package get or post function to make an API call.

Now, we can get project lists, get all tasks, get tasks by project, or create a new task. If you need any other functions to interact with Todoist API, you can add that to the APIHandler class.

How to write Telegram bot in Python

In this step, we are going to write the main class for Telegram bot. I’m creating another class for bot functions. Before that, we need to install python-telegram-bot, the python package that we are utilizing to create the Telegram bot.

Run pip installer command to install python-telegram-bot:

pip install python-telegram-bot

The general structure for using python-telegram-bot is like this

from telegram.ext import Updater, MessageHandler, CommandHandler, CallbackQueryHandler, Filters


bot_token = "YOUR_BOT_TOKEN"
updater = Updater(bot_token)

dp = updater.dispatcher

# Add command handler
dp.add_handler(CommandHandler('COMMAND_NAME', COMMAND_HANDLER))

# Add message handler
updater.dispatcher.add_handler(MessageHandler(Filters.all, MESSAGE_HANDLER))

# Add button handler
updater.dispatcher.add_handler(CallbackQueryHandler(BUTTON_HANDLER))

updater.start_polling()
updater.idle()

We need to create an instance of Updater with bot_token which is the access_token from the first step.

After that, for any command you define in your Telegram bot, you need to add a function to handle that command. This can be done by add_handler function of updater.dispatcher.

Also to interact with the bot users, we add a function to handle all receiving messages. Also, we add a function to handle every button a user taps on.

The final step is to call the start_polling function and the idle function of the updater. This way our script is waiting for commands, messages or any button tap.

Todoistbot class

Let’s create a class for our main bot script. Starting with __init__ function, we are reading bot_token, api_token and api_url configuration from the confi.ini file. After that, we create an instance of updater and an instance of APIHandler from our APIHandler class.

import configparser
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Updater, MessageHandler, CommandHandler, CallbackQueryHandler, Filters
from APIHandler import APIHandler


class TodoistBot:
    def __init__(self):
        # Read Configs from file
        config = configparser.ConfigParser()
        config.read("config.ini")

        # set Telegram bot token
        bot_token = config['telegram']['bot_token']
        # set Todoist API token
        api_token = config['todoist']['api_token']
        # set Todoist API URL
        api_url = config['todoist']['api_url']

        # initiate a Telegram updater instance
        self.updater = Updater(bot_token)

        # initiate a Todoist API handler
        self.api = APIHandler(api_token, api_url)

    def projects(self, bot, update):
        chat_id = update.message.chat_id
        project_list = self.api.get_project_list()

        keyboard = []
        for project in project_list:
            keyboard.append(
                [InlineKeyboardButton(project['name'], callback_data=project['id'])])

        reply_markup = InlineKeyboardMarkup(keyboard)
        bot.send_message(chat_id=chat_id, text="Choose a Project to see tasks list", reply_markup=reply_markup)

    def main(self):
        updater = self.updater
        dp = updater.dispatcher

        # Add command handlers
        dp.add_handler(CommandHandler('projects', self.projects))

        updater.start_polling()
        updater.idle()

We added a handler for projects command, which means every time a user writes /projects or choose it in our Telegram bot, it runs the function projects of our scripts.

How to show buttons for users in Telegram bot

First, you need to import InlineKeyboardButton and InlineKeyboardMarkup. As you can see in the projects function, the first parameter of the InlineKeyboardButton is the text of the button and callback_data is the data taping on the button will return. You can add any button you need to show to a list and give it to the InlineKeyboardMarkup object and pass the result with the send_message function. Also, you can add a URL to the button to make it as a link button.

I added a static function to generate a reply-markup for the tasks list. I am using it in showing all tasks and showing today tasks list in my Telegram bot.

@staticmethod
def task_button_markup(tasks):
    keyboard = []
    for task in tasks:
        keyboard.append(
            [InlineKeyboardButton(task['content'], url=task['url'], callback_data=task['id'])])

    markup = InlineKeyboardMarkup(keyboard)
    return markup

Getting user input in Telegram bot

We have a general handler function for messages. This function will receive any messages a user sends in Telegram bot. Also, there is another handler for buttons, so when our bot shows a list of projects as Telegram bot buttons, The handler function receives the callback_data which is registered with the button.

So every user input is either a command which runs a function in our script, a message which goes to general_handler function, or a button tap which goes to button function.

class TodoistBot:
# all other functions
# .
# .
# .

    # handler for buttons
    def button(self, bot, update):
        query = update.callback_query
    
    # general handler for messages
    def general_handler(self, bot, update):
        chat_id = update.message.chat_id
        text = update.message.text

    def main(self):
        updater = self.updater
        dp = updater.dispatcher

        # Add command handlers
        dp.add_handler(CommandHandler('projects', self.projects))
        # other commands will goes here

        # Add callback handlers for buttons
        updater.dispatcher.add_handler(CallbackQueryHandler(self.button))

        # general message handler
        updater.dispatcher.add_handler(MessageHandler(Filters.all, self.general_handler))

        updater.start_polling()
        updater.idle()

To see what users want to do, I assigned some flags to our class. So if the user sends the /new_taks command, I just set the new_task flag to True.

class TodoistBot:
    class Flags:
        new_project = False
        new_task = False
        select_project_for_task = False

        def __init__(self, flag=False):
            self.new_project = flag
            self.new_task = flag
            self.select_project_for_task = flag

    flags = Flags()

# All other functions
# ...

Now we can define a function to respond to the /new_task command.

def new_task(self, bot, update):
    chat_id = update.message.chat_id
    self.flags.new_task = True
    bot.send_message(chat_id=chat_id, text="enter name for new task")

And add the command handler to the dispatcher (the db variable) in the main function:

dp.add_handler(CommandHandler('newtask', self.new_task))

Now that we set the new_task flag to the True, in the general_handler function we can create a new task with the user’s input.

class TodoistBot:
    class Flags:
        new_task = False

        def __init__(self, flag=False):
            self.new_task = flag

    flags = Flags()

    def __init__(self):
      ...

    def new_task(self, bot, update):
        chat_id = update.message.chat_id
        self.flags.new_task = True
        bot.send_message(chat_id=chat_id, text="enter name for new task")

    def general_handler(self, bot, update):
        chat_id = update.message.chat_id
        text = update.message.text

        if self.flags.new_task:
            if self.api.create_task(text):
                bot.send_message(chat_id=chat_id, text="task created: " + text)

    def main(self):
        updater = self.updater
        dp = updater.dispatcher

        # Add command handlers
        # .
        # .
        # .
        dp.add_handler(CommandHandler('newtask', self.new_task))

        # ...

        # general message handler
        updater.dispatcher.add_handler(MessageHandler(Filters.all, self.general_handler))

        updater.start_polling()
        updater.idle()

The same logic can be implemented for the new project command and the function.

The End

Thanks for reading this, here is the GitHub code too: click here

Comments (0)