DEV Community

Cover image for Automating Grocery Lists with Notion and Telegram

Automating Grocery Lists with Notion and Telegram

TL;DR

Managing grocery lists manually was chaotic, so I automated the process using Notion and a Telegram bot. A TypeScript script connects to Notion’s API, gathers ingredients from selected recipes, and creates a shopping list automatically. The bot allows me to generate lists and mark items as purchased with simple commands.

Introduction

My partner and I recently decided to eat healthier, which meant cooking more at home. She shared several recipe links, but when I went grocery shopping, I realized managing ingredients manually was overwhelming. Jumping between recipe links while trying to buy everything efficiently was frustrating.

That’s when I had an idea: automate the whole process!

The Notion Setup

To keep track of everything, I used Notion, where we already manage household tasks. I created several databases:

  • Ingredients – A list of all the ingredients we might need.
  • Recipes – Each recipe links to the necessary ingredients.
  • Shopping Lists – A database where each entry represents a shopping trip, containing a to-do list of ingredients to buy.

This setup made organizing ingredients easier, but I still had to manually transfer them from recipes to the shopping list. Not efficient enough!

Automating with TypeScript and Notion’s API

To fully automate the process, I wrote a TypeScript script that connects to Notion’s API. Here’s what it does:

1- Scans all recipes where a specific checkbox is enabled.

async function getRecipesToAdd() {
  const response = await notion.databases.query({
    database_id: RECIPES_DB_ID, // Your notion db ID, you can grab it from the url
    filter: {
      property: "Add to list?", // The checkbox we manually enable if we want recipes to be processed
      checkbox: {
        equals: true,
      },
    },
  });
  return response.results;
}

Enter fullscreen mode Exit fullscreen mode

2- Extracts the required ingredients.

async function getIngredientsList(recipePage) {
  const relationArray = recipePage.properties["Ingredients"]?.relation;
  if (!relationArray || !relationArray.length) {
    return [];
  }

  const ingredientNames = [];

  for (const rel of relationArray) {
    const ingredientPageId = rel.id;

    const ingredientPage = await notion.pages.retrieve({
      page_id: ingredientPageId,
    });
    const nameProp = (ingredientPage as PageObjectResponse).properties[
      "Ingredient"
    ];
    let ingredientName = "Unnamed Ingredient";
    if (
      nameProp &&
      isTitleProperty(nameProp) &&
      nameProp.title &&
      nameProp.title[0]
    ) {
      ingredientName = nameProp.title[0].plain_text;
    }

    ingredientNames.push(ingredientName);
  }

  return ingredientNames;
}
Enter fullscreen mode Exit fullscreen mode

3- Creates a new shopping list.

async function createShoppingListPage() {
  const todayStr = new Date().toISOString().slice(0, 10);
  const pageName = `Nueva lista ${todayStr}`;

  return await notion.pages.create({
    parent: { database_id: SHOPPING_LISTS_DB_ID },
    properties: {
      Name: {
        title: [{ type: "text", text: { content: pageName } }],
      },
      Fecha: {
        date: { start: todayStr },
      },
      Comprado: {
        checkbox: false,
      },
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

4- Populate the page with ingredients, we are going to use to-do blocks

async function appendIngredientChecklist(pageId, ingredients) {
  const children = ingredients.map((item) => ({
    object: "block",
    type: "to_do",
    to_do: {
      rich_text: [
        {
          type: "text",
          text: { content: item },
        },
      ],
      checked: false,
    },
  }));

  await notion.blocks.children.append({
    block_id: pageId,
    children,
  });
}
Enter fullscreen mode Exit fullscreen mode

This meant that with a simple selection of recipes, my shopping list would be generated instantly.

Running the Script via Telegram

I didn’t want to manually run the script on my computer every time, so I integrated it with Telegram:

  • I built a Telegram bot using telegraf, that triggers the script with a command.
  • The bot automatically compiles the grocery list inside Notion.
bot.command("import", async (ctx) => {
  const isValid = await checkUserValid(ctx.from.username, ctx); // Just a check making sure my partner and I are the only ones allowed to use certain commands
  if (!isValid) {
    return;
  }
  await ctx.reply("Importing ingredients...");
  return buildShoppingList(ctx);
});
Enter fullscreen mode Exit fullscreen mode
  • A second command lists pending shopping lists.
export async function listShoppingLists(context: Context) {
  await context.reply("Loading shopping list...");
  const response = await notion.databases.query({
    database_id: process.env.SHOPPING_LIST_DB_ID,
    filter: {
      property: "Bought",
      checkbox: {
        equals: false,
      },
    },
  });

  if (response.results.length > 0) {
    await context.reply(
      `${context.from.first_name}, elements pending to buy:`,
    );
  } else {
    return context.reply("No elements to add.");
  }

  for (const page of response.results) {
    const allIngredients: string[] = [];
    const ingredients = await notion.blocks.children.list({
      block_id: page.id,
    });
    allIngredients.push(
      ...ingredients.results
        .map((ingredient: BlockObjectResponse) => {
          if (ingredient.type === "to_do") {
            const checked = ingredient.to_do.checked ? "" : "🔲";
            return `${checked} ${ingredient.to_do.rich_text[0].plain_text}`;
          }
          return null;
        })
        .filter(Boolean),
    );
    if (allIngredients.length > 0) {
      const inlineKeyboard = Markup.inlineKeyboard([
        Markup.button.callback(
          "Mark as bought",
          `markPurchased:${page.id}`,
        ),
      ]);
      const nameProp = (page as PageObjectResponse).properties["Name"];
      if (isTitleProperty(nameProp)) {
        await context.reply(`${nameProp.title[0].plain_text}:`);
      }
      await context.reply(allIngredients.join("\n"), inlineKeyboard);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  • A button allows marking a list as purchased (added in the code above) and its handler.
bot.action(/markPurchased:(.+)/, async (ctx) => {
  const isValid = await checkUserValid(ctx.from.username, ctx);
  if (!isValid) {
    return;
  }
  const pageId = ctx.match[1];
  await markAsPurchased(ctx, pageId);
});  
Enter fullscreen mode Exit fullscreen mode

Now, with a quick /import command, I get my groceries organized effortlessly.

Next Steps

I initially built this for personal use, but it was also a great way to experiment with Notion’s API. Some ideas for future improvements:

  • Recipe scaling: Adjust ingredient quantities dynamically based on portions.
  • Multiple store support: Categorize ingredients by where to buy them.
  • Something not related to groceries!: Maybe a library and a local search on Telegram to know if you have a certain book?

What Would You Add?

This has been a fun side project, but I’d love to hear ideas for making it even better. Would you find something like this useful? Let me know in the comments!

Top comments (0)