Build an AI News Buddy on Telegram

Automate a daily Telegram digest using Python, Gemini, and GitHub Actions.

Introduction

30 Second Summary

You subscribe to newsletters, follow tech feeds, and save articles you never read. By the time you catch up, the news is stale and the summaries feel like they were written by a corporate robot.

In this project, you will build an AI-powered news assistant that messages you every morning on Telegram with a casual, friend-like digest covering AI, log analysis, incident diagnosis, and European AI policy.

What You'll Build

You open Telegram in the morning and see a message from your AI News Buddy. It reads like a text from a knowledgeable friend who stayed up reading the news so you didn't have to.

By the end of this project, you'll have:

  • A daily Telegram message that arrives every morning with curated news across four domains, written in a tone that feels like catching up with a knowledgeable friend over coffee.
  • A persona prompt you crafted yourself that transforms dry RSS feed data into casual, opinionated summaries you actually want to read.
  • A fully automated pipeline running on GitHub Actions that fetches, summarizes with Gemini, and delivers your digest every morning at zero cost with zero intervention.
  • Secret Mission: An overflow queue that saves excess stories for tomorrow's digest, giving your buddy a "memory" across days.

Are there any prerequisites?

You should be comfortable with basic Python (variables, functions, and loops). You also need a GitHub account and a Telegram account.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters to you personally.

Set Up Your Environment

Your AI News Buddy needs three things to work: Python to run the script, a Gemini API key to generate summaries, and a Telegram bot to deliver messages to your phone.

In this step, you will install Python, grab both API credentials, and wire up a project folder with all the dependencies ready to go.

In this step, get ready to:
  • Verify Python 3.11+ is installed and create your project folder.
  • Get a Gemini API key and create a Telegram bot.
  • Install project dependencies and configure your secrets.
Verify Python and create your project folder

Python is the language your script will be written in. You need version 3.11 or higher because the libraries this project depends on require it.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Check your Python version by running this command:
python3 --version

✔️ I see version 3.11 or higher

You are all set. Move on to creating your project folder below.

ⓧ I see an older version

macOS often ships with an older Python that shadows newer installs. Install Python 3.11 via Homebrew by running:

brew install python@3.11

After installation completes, run the version check again:

python3 --version

If you still see an older version, Homebrew's Python may not be first on your PATH. Run /opt/homebrew/bin/python3 --version to confirm Homebrew's copy is 3.11+, then use /opt/homebrew/bin/python3 for the rest of this project.

ⓧ Command not found

Python is not installed yet. Install it via Homebrew by running:

brew install python@3.11

Once complete, verify the install:

python3 --version

You should now see Python 3.11.x or higher printed in the terminal.

Homebrew not found?

If you see brew: command not found, install Homebrew first by following the instructions at https://brew.sh. Then re-run the Python install command above.

help me install Homebrew and Python on my Mac

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir ai-news-buddy
cd ai-news-buddy
  • Create a virtual environment and activate it by running:
python3 -m venv venv
source venv/bin/activate

You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.

What is a virtual environment?

A virtual environment is an isolated space for your project's Python packages. It keeps this project's dependencies separate from other Python projects on your machine.

Don't see (venv) in your prompt?

Make sure you ran source venv/bin/activate (not just the python3 -m venv venv line). The first command creates the environment. The second activates it.

help me activate my Python virtual environment

Get your Gemini API key and create a Telegram bot

Your script needs two credentials: a Gemini API key so it can ask the AI to summarize news, and a Telegram bot token so it can send messages to your phone.

  • Open your browser and go to https://aistudio.google.com/apikey.
  • Sign in with your Google account.
  • Click Create API key.
  • Copy the key and save it somewhere safe (a notes app or password manager). You will need it in a moment.

Is the Gemini API free?

Yes. The free tier gives you 1,500 requests per day. Your News Buddy only makes one request per day, so you will never come close to the limit.

Now create the Telegram bot that will deliver your daily digest.

  • Open Telegram on your phone or desktop.
  • Search for @BotFather and open the chat.
  • Send the message /newbot.
  • BotFather will ask you for a display name. Type any name you like (e.g. AI News Buddy).
  • BotFather will then ask for a username. This must end in bot (e.g. my_news_buddy_bot).
  • BotFather replies with your bot token. Copy it and save it alongside your Gemini key.

The token looks something like 123456789:ABCdefGHI-jklMNOpqrSTUvwx. Keep it private.

Next, you need your personal chat ID so the bot knows where to send messages.

  • In Telegram, find your new bot by searching for the username you just created.
  • Open the chat with your bot and send /start.
  • Open your browser and paste the following URL, replacing YOUR_BOT_TOKEN_HERE with the token you just copied:
https://api.telegram.org/botYOUR_BOT_TOKEN_HERE/getUpdates
  • In the JSON response, look for "chat":{"id": followed by a number. That number is your chat ID.
  • Copy the chat ID number and save it with your other credentials.

What does the getUpdates response look like?

You will see a JSON object containing a result array. Each item has a message object with a chat field. Your chat ID is the numeric id value inside chat. It is usually a 9-10 digit number.

Seeing an empty result array?

Make sure you sent /start to your bot BEFORE you opened the getUpdates URL. If you did and it is still empty, wait 10 seconds and refresh the page.

help me find my Telegram chat ID

Install dependencies and configure your secrets

Your project needs three Python packages: the Google GenAI SDK for talking to Gemini, feedparser for reading RSS feeds, and requests for sending HTTP calls to Telegram.

  • Create requirements.txt in your ai-news-buddy folder by running this command:
cat << 'EOF' > requirements.txt
google-genai>=2.16.0,<3.0.0
feedparser==6.0.13
requests>=2.34.0,<3.0.0
EOF

What does this command do?

The cat << 'EOF' > requirements.txt pattern writes everything between the two EOF markers into the file. It is a quick way to create a multi-line file without opening an editor.

  • Install the dependencies by running:
pip install -r requirements.txt

You should see pip downloading and installing all three packages. The final line should show Successfully installed followed by package names.

Install failing with a permissions error?

Make sure your virtual environment is active. You should see (venv) in your terminal prompt. If not, run source venv/bin/activate and try the install again.

help me fix pip install errors in my virtual environment

Now create the .env file to store your credentials securely.

  • Create .env by running this command (replace each placeholder with the real values you saved earlier):
cat << 'EOF' > .env
GEMINI_API_KEY=your-gemini-api-key-here
TELEGRAM_BOT_TOKEN=your-telegram-bot-token-here
TELEGRAM_CHAT_ID=your-chat-id-here
EOF
  • Open .env in a text editor and replace each placeholder with your actual credentials.

Why use a .env file?

A .env file keeps secrets out of your code. Your script reads these values at runtime. When you push to GitHub later, you will add .env to .gitignore so it never gets committed.

  • Confirm your files exist by running:
ls -a

You should see .env, requirements.txt, and the venv folder listed in the output.

Missing .env in the listing?

Files starting with a dot are hidden by default. Make sure you used ls -a (with the -a flag) to show hidden files.

help me find my .env file on macOS

✔️ Awesome, I've got everything!

Great. Make sure you have saved your .env file with your real credentials.

ⓧ I'd like to double check the full code

Your requirements.txt should contain exactly this:

google-genai>=2.16.0,<3.0.0
feedparser==6.0.13
requests>=2.34.0,<3.0.0

Your .env should follow this structure (with your real values in place of the placeholders):

GEMINI_API_KEY=<your_gemini_api_key>
TELEGRAM_BOT_TOKEN=<your_telegram_bot_token>
TELEGRAM_CHAT_ID=<your_telegram_chat_id>

Your environment is locked and loaded. You have Python running, both API credentials saved, and all dependencies installed. Next up, you will write the code that pulls fresh news from RSS feeds.

Fetch News from RSS Feeds

Your environment is set up with Python, your API keys, and a Telegram bot ready to go. Now you need raw material for the AI to work with.

Before Gemini can summarize anything, your script needs fresh articles to read. In this step, you will pull articles from RSS feeds across four domains and see the contrast between raw feed data and what you will eventually transform it into.

In this step, get ready to:
  • Create a feed configuration file with URLs organized by domain.
  • Build a feed fetcher that pulls and filters articles from the last 24 hours.
  • Run the script and verify articles are printing from all four domains.
Create your feed configuration

Your news buddy needs to know where to find articles. You will organize feed URLs into four domains: AI general news, log analysis research, incident diagnosis, and AI in France/Europe.

  • Create a new file called config.py in your project folder by running:
touch config.py
  • Open config.py in your editor and paste the following code:
"""RSS feed configuration organized by domain."""

FEEDS = {
    "ai_general": [
        "https://news.google.com/rss/search?q=artificial+intelligence&hl=en-US&gl=US&ceid=US:en",
        "https://www.technologyreview.com/feed/",
        "https://openai.com/blog/rss.xml",
    ],
    "log_analysis": [
        "https://arxiv.org/rss/cs.SE",
        "https://arxiv.org/rss/cs.LG",
    ],
    "incident_diagnosis": [
        "https://arxiv.org/rss/cs.DC",
        "https://arxiv.org/rss/cs.NI",
    ],
    "ai_europe": [
        "https://news.google.com/rss/search?q=intelligence+artificielle+France&hl=fr&gl=FR&ceid=FR:fr",
        "https://www.euractiv.com/sections/digital/feed/",
    ],
}

TELEGRAM_MESSAGE_LIMIT = 4096
MAX_ARTICLES_PER_DOMAIN = 5
TOTAL_TARGET_CHARS = 3000

What does this code do?

The FEEDS dictionary maps each domain to a list of RSS feed URLs. Each domain covers a different area of interest: general AI news, software engineering and machine learning research (for log analysis), distributed computing and networking research (for incident diagnosis), and French/European AI policy.

The three constants at the bottom control message limits. MAX_ARTICLES_PER_DOMAIN caps how many articles to pull from each feed, keeping the digest manageable.

  • Save config.py.
  • Verify the config loads correctly by running:
python3 -c "from config import FEEDS; print(f'{len(FEEDS)} domains configured')"

You should see 4 domains configured printed in your terminal.

Seeing a ModuleNotFoundError?

Make sure you are running the command from inside your project folder (the same directory where config.py lives).

Also check that your virtual environment is still active. You should see (venv) at the start of your terminal prompt. help me fix my ModuleNotFoundError when importing config

✔️ Awesome, I've got everything!

Great. Make sure config.py is saved and move on to building the news fetcher.

ⓧ I'd like to double check the full code

"""RSS feed configuration organized by domain."""

FEEDS = {
    "ai_general": [
        "https://news.google.com/rss/search?q=artificial+intelligence&hl=en-US&gl=US&ceid=US:en",
        "https://www.technologyreview.com/feed/",
        "https://openai.com/blog/rss.xml",
    ],
    "log_analysis": [
        "https://arxiv.org/rss/cs.SE",
        "https://arxiv.org/rss/cs.LG",
    ],
    "incident_diagnosis": [
        "https://arxiv.org/rss/cs.DC",
        "https://arxiv.org/rss/cs.NI",
    ],
    "ai_europe": [
        "https://news.google.com/rss/search?q=intelligence+artificielle+France&hl=fr&gl=FR&ceid=FR:fr",
        "https://www.euractiv.com/sections/digital/feed/",
    ],
}

TELEGRAM_MESSAGE_LIMIT = 4096
MAX_ARTICLES_PER_DOMAIN = 5
TOTAL_TARGET_CHARS = 3000
Build the news fetcher

Now you need a script that reads those feed URLs, pulls the articles, and filters them to the last 24 hours. The feedparser library handles all the RSS parsing for you.

  • Create a new file called news_buddy.py by running:
touch news_buddy.py
  • Open news_buddy.py in your editor and paste the imports and environment loader:
"""AI News Buddy - fetches, summarizes, and delivers daily news via Telegram."""

import os
import json
from datetime import datetime, timedelta, timezone

import feedparser
import requests
from google import genai
from google.genai import types

from config import FEEDS, TELEGRAM_MESSAGE_LIMIT, MAX_ARTICLES_PER_DOMAIN, TOTAL_TARGET_CHARS


def load_env():
    """Load environment variables from .env file if it exists (local dev)."""
    env_path = os.path.join(os.path.dirname(__file__), ".env")
    if os.path.exists(env_path):
        with open(env_path) as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith("#") and "=" in line:
                    key, value = line.split("=", 1)
                    os.environ.setdefault(key.strip(), value.strip())

What does this code do?

  • The top imports bring in everything the script needs: feedparser for RSS parsing, requests for HTTP calls, and the google-genai SDK for Gemini.
  • The from config import ... line pulls in the feed URLs and constants you just created.
  • The load_env() function reads your .env file line by line and loads each key-value pair into the environment. This lets the script find your API keys when running locally.
  • Save news_buddy.py.
  • Now add the fetch_feeds() function below load_env(). Leave a blank line after the last line of load_env(), then paste:
def fetch_feeds():
    """Fetch articles from all configured RSS feeds, filtered to last 24 hours."""
    cutoff = datetime.now(timezone.utc) - timedelta(hours=24)
    articles = []

    for domain, urls in FEEDS.items():
        for url in urls:
            try:
                feed = feedparser.parse(url)
                for entry in feed.entries[:MAX_ARTICLES_PER_DOMAIN]:
                    published = entry.get("published_parsed")
                    if published:
                        pub_dt = datetime(*published[:6], tzinfo=timezone.utc)
                        if pub_dt < cutoff:
                            continue

                    articles.append({
                        "title": entry.get("title", "No title"),
                        "summary": entry.get("summary", "")[:300],
                        "link": entry.get("link", ""),
                        "domain": domain,
                    })
            except Exception as e:
                print(f"Error fetching {url}: {e}")

    return articles

What does this code do?

  • The cutoff variable calculates a timestamp 24 hours ago. Any article older than that gets skipped, so you only see fresh news.
  • The nested loop walks every domain and every feed URL, calling feedparser.parse() to pull the RSS data.
  • The [:MAX_ARTICLES_PER_DOMAIN] slice limits each feed to 5 entries, preventing any single source from flooding the digest.
  • Each article gets stored as a dictionary with its title, summary (truncated to 300 characters), link, and source domain.
  • The try/except block catches network errors gracefully so one broken feed does not crash the whole script.
  • Save news_buddy.py.

Seeing an ImportError on save?

If your editor shows a red underline on import feedparser or from google import genai, make sure your editor is using the Python interpreter from your virtual environment, not the system Python.

In VS Code, press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows), type Python: Select Interpreter, and choose the one inside your venv folder. help me fix import errors in my virtual environment

Run your first news fetch

Your fetcher is ready. Now you need a main() function to wire it all together and print what it finds.

  • At the bottom of news_buddy.py, below the fetch_feeds() function, add the following code:
def main():
    """Main pipeline: fetch and print articles."""
    load_env()
    print("Fetching feeds...")
    articles = fetch_feeds()
    print(f"Found {len(articles)} articles")
    for a in articles:
        print(f"[{a['domain']}] {a['title']}")


if __name__ == "__main__":
    main()

What does this code do?

  • The main() function is the entry point: it loads environment variables, fetches all feeds, then prints each article's domain and title.
  • The if __name__ == "__main__" guard ensures main() only runs when you execute the file directly, not when importing it as a module.
  • Save news_buddy.py.

Before you run this, what do you expect to see? Think about how many feeds you configured and how many articles each feed might have from the last 24 hours.

  • Run the script by executing:
python3 news_buddy.py

You should see output starting with Fetching feeds... followed by a count and a list of article titles grouped by domain tags like [ai_general], [log_analysis], [incident_diagnosis], and [ai_europe]. You should see roughly 15-20 articles total.

What should I see?

The exact articles will depend on what was published in the last 24 hours. Some feeds (like arXiv) may return fewer or more articles depending on the day. As long as you see articles from at least two or three of the four domains, your fetcher is working correctly.

Seeing 0 articles or only errors?

  • If you see Found 0 articles, it may be a quiet news day or the 24-hour filter is too strict. Try changing timedelta(hours=24) to timedelta(hours=72) temporarily to test.
  • If you see connection errors for specific URLs, your network may be blocking them. The script continues past broken feeds, so a few errors alongside successful results is normal.
  • If you see ModuleNotFoundError: No module named 'feedparser', your virtual environment may not be active. Run source venv/bin/activate and try again.

help me debug why my news_buddy.py isn't fetching articles

✔️ Awesome, I've got everything!

Great. Confirm you have saved both config.py and news_buddy.py before moving on.

ⓧ I'd like to double check the full code

Your news_buddy.py should contain these sections in order. Scroll up and confirm each one is present:

  • Module docstring and imports (os, json, datetime, feedparser, requests, genai, types, and config imports).
  • The load_env() function that reads .env and sets environment variables.
  • The fetch_feeds() function with the 24-hour cutoff filter and article parsing loop.
  • The main() function and if __name__ == "__main__" guard at the bottom.

You have raw articles flowing in from real RSS feeds. Next up, you will send these articles to Gemini and craft a persona prompt that transforms this raw data into a casual, friend-like message.

Make Gemini Write Like Your Best Friend

You have raw RSS articles printing in your terminal. Useful, but overwhelming. Nobody wants to read 20 raw headlines and summaries over breakfast.

This step transforms that raw data into a conversational message using the Gemini API. You will craft a persona prompt that makes the AI write like a friend texting you. The difference between a good prompt and a bad one here is the difference between a dry corporate newsletter and a message you actually look forward to reading.

In this step, get ready to:
  • Create a summarize() function with a carefully crafted friend persona prompt.
  • Call the Gemini API with length-limiting configuration to keep messages Telegram-friendly.
  • Wire everything into main() and see the casual summary printed to your terminal.
Create the summarize function with a friend persona

The summarize() function takes your raw articles, formats them for Gemini, and uses a detailed system instruction to control the AI's tone and style.

  • In news_buddy.py, find the line that says return articles at the end of the fetch_feeds() function. Add the following new function below it, with a blank line separating them:
def summarize(articles):
    """Send articles to Gemini and get a friend-like summary."""
    if not articles:
        return "Hey! Quiet day today, nothing major popped up in your feeds. Enjoy the silence!"

    articles_text = ""
    for article in articles:
        articles_text += f"[{article['domain']}] {article['title']}\n{article['summary']}\n\n"

    system_instruction = """You are my best friend who happens to be incredibly knowledgeable about tech, 
AI, log analysis, incident management, and European AI policy. Every morning you send me a casual 
Telegram message catching me up on what happened overnight.

Rules for your message:
- Write like you're texting a close friend. Use casual language, contractions, occasional slang.
- Be opinionated. Say what YOU think is interesting or important.
- Use short paragraphs. This is a chat message, not an essay.
- Group news by topic but use natural transitions, not formal headers.
- If something is boring, skip it. Only include what's genuinely interesting.
- Keep the total message under 3000 characters.
- Use emoji sparingly (1-2 per section max).
- Start with a casual greeting that varies each day.
- End with something encouraging or a fun sign-off.
- For log analysis and incident diagnosis: focus on methods, tools, and practical insights.
- For AI Europe: mention company names, people, and regulations specifically."""

What does this code do?

The first few lines handle the edge case where no articles were fetched. Instead of crashing, it returns a friendly "quiet day" message.

The articles_text loop formats each article as a tagged string so Gemini knows which domain it belongs to. The system_instruction is where the real magic happens. It defines the AI's entire personality: casual tone, opinionated stance, emoji restraint, domain-specific focus areas, and a hard character limit.

Each rule in the prompt steers Gemini away from its default "helpful assistant" mode and toward the friend-texting-you voice you want.

Call the Gemini API with length limits

The system instruction tells Gemini what to say. Now you need to actually call the API with configuration that enforces brevity.

  • Still inside the summarize() function, add the following code directly below the system_instruction variable (still indented inside the function):
    client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])

    response = client.models.generate_content(
        model="gemini-2.0-flash",
        contents=f"Here are today's news articles to summarize:\n\n{articles_text}",
        config=types.GenerateContentConfig(
            system_instruction=system_instruction,
            max_output_tokens=1000,
            temperature=0.9,
        ),
    )

    return response.text

What does this code do?

The genai.Client creates a connection to Google's API using your key from the environment. The generate_content call sends your formatted articles alongside the system instruction.

Two config values control output length. max_output_tokens=1000 caps how much text Gemini can generate (roughly 3000 characters). temperature=0.9 makes the output more creative and varied. A lower temperature would produce more predictable, robotic text. Combined with the "under 3000 characters" rule in the prompt, this keeps messages Telegram-friendly.

Wire summarize into main and run the pipeline

Your summarize() function is ready. Now update main() to call it instead of printing raw articles.

  • In news_buddy.py, scroll down to your existing main() function. Find these lines:
def main():
    """Main pipeline: fetch and print articles."""
    load_env()
    print("Fetching feeds...")
    articles = fetch_feeds()
    print(f"Found {len(articles)} articles")
    for a in articles:
        print(f"[{a['domain']}] {a['title']}")
  • Replace the entire main() function with this updated version:
def main():
    """Main pipeline: fetch, summarize, print."""
    load_env()
    print("Fetching feeds...")
    articles = fetch_feeds()
    print(f"Found {len(articles)} articles")

    print("Summarizing with Gemini...")
    message = summarize(articles)
    print(f"Message ({len(message)} chars):")
    print(message)

What changed?

Instead of looping through raw articles, main() now passes them to summarize() and prints the resulting message. The character count printout helps you confirm the output stays within Telegram's limits.

  • Save news_buddy.py.

Before you run this, what do you think the output will look like? A formal summary, or something closer to a text message from a friend?

  • Run the script to see your AI buddy in action:
python3 news_buddy.py

You should see a casual, conversational message printed to your terminal. It reads like a friend catching you up on the news, not a corporate email. The character count should be under 3000.

Seeing an API error?

  • Check that your .env file has a valid GEMINI_API_KEY value. Open .env and confirm the key is pasted correctly with no extra spaces.
  • If you see a quota or rate limit error, wait 60 seconds and try again. The free tier allows 1,500 requests per day.
  • If you see an indentation error, make sure the client = genai.Client(...) line and everything after it is indented inside the summarize() function (4 spaces).

Why is my Gemini API call failing in news_buddy.py?

Why a high temperature?

Temperature controls how "random" the model's word choices are. At 0.9, Gemini picks more surprising words and phrases. This makes the output feel natural and varied, like a real person who doesn't repeat the same greeting every morning. A temperature of 0.2 would produce predictable, safe text every time.

✔️ Awesome, I've got everything!

Great. Double check you have saved news_buddy.py and that your terminal output shows a casual friend-like message.

ⓧ I'd like to double check the full code

Your news_buddy.py now has four major sections. Scroll through and confirm each one is present in this order:

  • Imports and load_env() function (unchanged from before).
  • fetch_feeds() function (unchanged from before).
  • summarize() function with the early return, articles_text formatting, system_instruction string, genai.Client call, and return response.text.
  • main() function that calls fetch_feeds(), then summarize(), then prints the message with its character count.
  • if __name__ == "__main__": main() at the bottom.

Your AI buddy now writes like a real friend. The message is casual, opinionated, and concise. Next up, you will send this message straight to your Telegram so it arrives on your phone like a text from a mate.

Deliver to Telegram

You have a great message sitting in your terminal. Right now it reads like a buddy texting you about today's news, but nobody can actually read it except you.

In this step, you connect the script to the Telegram Bot API so the message lands on your phone like a friend texted you. One function, one HTTP call, and your bot goes from terminal output to real delivery.

In this step, get ready to:
  • Create a send_telegram() function that POSTs messages to your bot.
  • Handle Telegram's 4096-character message limit with automatic splitting.
  • Run the full pipeline end-to-end and receive a message on your phone.
Create the send_telegram() function

The Telegram Bot API exposes a simple HTTP endpoint for sending messages. Your function POSTs a JSON payload to that endpoint with your chat ID and message text.

  • In news_buddy.py, find the end of the summarize() function (the return response.text line).
  • Add this new function below it:
def send_telegram(message):
    """Send a message via Telegram Bot API."""
    token = os.environ["TELEGRAM_BOT_TOKEN"]
    chat_id = os.environ["TELEGRAM_CHAT_ID"]
    url = f"https://api.telegram.org/bot{token}/sendMessage"

    # Split message if it exceeds Telegram limit
    if len(message) <= TELEGRAM_MESSAGE_LIMIT:
        chunks = [message]
    else:
        chunks = [message[i:i + TELEGRAM_MESSAGE_LIMIT] for i in range(0, len(message), TELEGRAM_MESSAGE_LIMIT)]

    for chunk in chunks:
        payload = {
            "chat_id": chat_id,
            "text": chunk,
            "parse_mode": "Markdown",
        }
        resp = requests.post(url, json=payload)
        if resp.status_code != 200:
            # Retry without Markdown if parsing fails
            payload["parse_mode"] = ""
            resp = requests.post(url, json=payload)
        resp.raise_for_status()

What does this code do?

  • The function reads your bot token and chat ID from environment variables, then builds the Telegram sendMessage endpoint URL.
  • If the message exceeds 4096 characters (Telegram's limit), it splits the text into chunks so nothing gets cut off.
  • Each chunk is sent with parse_mode set to Markdown for nice formatting. If Telegram rejects the Markdown (status not 200), the function retries without formatting so the message still arrives.

Why retry without Markdown?

Gemini's output sometimes includes characters that break Telegram's Markdown parser (unmatched underscores, stray asterisks). Instead of losing the whole message, the retry sends it as plain text. You still get the content, just without bold or italic formatting.

Wire send_telegram() into main() and run the full pipeline

The function exists but nothing calls it yet. You need to update main() to send the summarized message after printing it.

  • In news_buddy.py, scroll down to the main() function.
  • Find the last line of main(), which is:
    print(message)
  • Add these lines directly below it, inside main():
    print("Sending to Telegram...")
    send_telegram(message)
    print("Done!")
  • Also update the docstring at the top of main() from """Main pipeline: fetch, summarize, print.""" to:
    """Main pipeline: fetch, summarize, send."""
  • Save news_buddy.py.

Before you run this, think about what you expect to happen. Will the message arrive on your phone? Will it look like a system notification or a friend's message?

  • Run the full pipeline by running this command:
python3 news_buddy.py

Check your phone. You should see a Telegram message from your bot that reads like a casual friend update, not a system notification.

What should I see?

Your terminal prints the article count, the summarized message, and then Sending to Telegram... followed by Done!. On your phone, you get a Telegram notification from your bot with the same casual summary you saw printed in the terminal.

No message arriving?

  • Make sure you sent /start to your bot in Telegram first. Your bot cannot message you until you initiate contact.
  • Double-check your TELEGRAM_CHAT_ID in .env. If the ID is wrong, the API call fails silently or returns an error.
  • Check for a requests.exceptions.HTTPError in your terminal. This means the Telegram API rejected the request. Verify your TELEGRAM_BOT_TOKEN is correct.

Still stuck? Help me debug why my Telegram bot isn't sending messages.

✔️ Awesome, I've got everything!

Great. Double check you have saved news_buddy.py with both send_telegram() and the updated main().

ⓧ I'd like to double check the full code

Your news_buddy.py should contain these sections in order. Scroll up and confirm each is present:

  • Imports at the top (os, json, datetime, feedparser, requests, genai, types, and the config imports).
  • load_env() function that reads the .env file.
  • fetch_feeds() function that pulls RSS articles filtered to the last 24 hours.
  • summarize() function that sends articles to Gemini with the friend persona prompt.
  • send_telegram() function that POSTs to the Telegram Bot API with message splitting and Markdown retry.
  • main() function that calls fetch_feeds(), summarize(), and send_telegram() in sequence.
  • The if __name__ == "__main__": guard at the bottom calling main().

Your bot is live. Every time you run the script, fresh news arrives on your phone as a casual message from your AI buddy. Next up, you will automate this so it happens every morning without you lifting a finger.

Automate with GitHub Actions

Your script works perfectly on your machine. You run it, Gemini summarizes the news, and your Telegram lights up with a friendly digest.

The problem is you have to remember to run it every morning. That defeats the purpose of automation. GitHub Actions solves this by running your script on a schedule, for free, with zero infrastructure to manage.

In this step, get ready to:
  • Create a GitHub repository and add a workflow file that schedules your script to run daily.
  • Store your API keys securely as repository secrets.
  • Trigger the workflow manually and verify the Telegram message arrives.
Create a GitHub repository and workflow file

Your code needs to live on GitHub so that GitHub Actions can access it. You will create a new public repository and then add a workflow configuration file that tells GitHub exactly when and how to run your script.

  • Open your browser and go to https://github.com/new to create a new repository.
  • Enter ai-news-buddy in the Repository name field.
  • Select Public for visibility (GitHub Actions is free for public repos).
  • Leave all other options unchecked (no README, no .gitignore, no license).
  • Click Create repository.

GitHub shows you a Quick Setup page with instructions. Keep this page open because you will need the repository URL shortly.

  • Back in your terminal, make sure you are inside your project folder (the one containing news_buddy.py).
  • Create the workflow directory structure by running this command:
mkdir -p .github/workflows
  • Confirm the directory was created by running:
ls .github/workflows

You should see an empty directory listed (no errors). This is where your workflow YAML file will live.

  • Create the workflow file by running:
touch .github/workflows/daily-digest.yml
  • Open .github/workflows/daily-digest.yml in your code editor and paste the following configuration:
name: AI News Buddy Daily Digest

on:
  schedule:
    - cron: '0 5 * * *'
  workflow_dispatch:

jobs:
  send-digest:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Run news buddy
        env:
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
        run: python news_buddy.py

What does this workflow do?

The schedule trigger uses cron syntax. '0 5 * * *' means "at minute 0 of hour 5, every day" in UTC. That is 5:00 AM UTC daily.

The workflow_dispatch trigger adds a manual "Run workflow" button in the GitHub UI so you can test without waiting until 5 AM.

The job checks out your code, installs Python 3.11, installs your requirements.txt packages, and runs news_buddy.py with your secrets injected as environment variables.

Your load_env() function already handles this. It uses os.environ.setdefault() which only sets a value if one is not already present. On GitHub Actions, the secrets are injected via the env: block before your script runs, so load_env() gracefully does nothing.

  • Save .github/workflows/daily-digest.yml.

Indentation matters in YAML

YAML is whitespace-sensitive. Make sure you use spaces (not tabs) and match the indentation exactly as shown. A single misaligned space can break the workflow. If you see parsing errors later, double-check the indentation of your steps: section.

Need help? Why is my workflow YAML failing to parse?

Add your secrets to GitHub

Your .env file holds secrets locally, but you must never push it to GitHub. Instead, GitHub provides encrypted repository secrets that get injected into your workflow at runtime.

  • In your browser, navigate to your ai-news-buddy repository on GitHub.
  • Click Settings in the repository navigation tabs.
  • In the left sidebar, under the Security section, click Secrets and variables, then click Actions.
  • Click the Secrets tab.
  • Click New repository secret.
  • In the Name field, type GEMINI_API_KEY.
  • In the Secret field, paste your Gemini API key value (the same one from your .env file).
  • Click Add secret.
  • Repeat the process to add your second secret: click New repository secret, enter TELEGRAM_BOT_TOKEN as the name, paste your bot token as the value, and click Add secret.
  • Repeat once more for the third secret: name it TELEGRAM_CHAT_ID, paste your chat ID, and click Add secret.

You should now see all three secrets listed on the Actions secrets page: GEMINI_API_KEY, TELEGRAM_BOT_TOKEN, and TELEGRAM_CHAT_ID.

Why secrets instead of environment variables?

GitHub encrypts secrets at rest and only decrypts them during workflow execution. They never appear in logs (GitHub auto-redacts them). This means your API keys stay safe even in a public repository.

Push your code and trigger the workflow

With the workflow file written and secrets stored, you can now push your code to GitHub and test the full pipeline.

  • Initialize a git repository in your project folder by running:
git init
  • Stage only the files that should be public (not your .env) by running:
git add config.py news_buddy.py requirements.txt .github/

Why not git add everything?

Your .env file contains your real API keys and tokens. Pushing it to a public repository would expose your credentials to anyone on the internet. By adding files individually, you ensure .env stays local.

  • Create your first commit by running:
git commit -m "Initial commit: AI News Buddy"
  • Connect your local repository to GitHub and push by running these commands:
git branch -M main
git remote add origin [[REPO_URL="https://github.com/YOUR-USERNAME/ai-news-buddy.git"]]
git push -u origin main

Where do I find my repository URL?

Go back to the Quick Setup page that appeared when you created the repository. Copy the HTTPS URL shown at the top. It follows the pattern https://github.com/your-username/ai-news-buddy.git.

After the push completes, refresh your repository page on GitHub. You should see config.py, news_buddy.py, requirements.txt, and the .github/workflows folder listed.

Push rejected or authentication failed?

GitHub no longer accepts passwords for git push. You need a Personal Access Token or SSH key configured. If prompted for credentials, create a token at Settings > Developer settings > Personal access tokens on GitHub and use it as your password.

Need help? Why is my git push to GitHub failing with authentication errors?

Now trigger the workflow manually to verify everything works end-to-end.

  • On your repository page, click the Actions tab.
  • In the left sidebar, click AI News Buddy Daily Digest (the name of your workflow).
  • Click the Run workflow button.
  • Leave the branch set to main and click Run workflow again to confirm.

Before you check your phone, what do you expect to happen? The workflow should install your dependencies, run news_buddy.py, fetch RSS feeds, summarize them with Gemini, and send the result to your Telegram.

  • Wait about 1-2 minutes for the workflow to complete. You can watch the progress by clicking on the running workflow in the Actions tab.
  • Check your Telegram. You should receive a message from your bot with today's news digest.

You should see a green checkmark next to the workflow run in the Actions tab, and a fresh news digest in your Telegram chat.

Workflow failed with a red X?

Click the failed run to see the logs. Common causes:

  • Secret names don't match. Make sure you named them exactly GEMINI_API_KEY, TELEGRAM_BOT_TOKEN, and TELEGRAM_CHAT_ID (case-sensitive).
  • YAML indentation is wrong. Compare your file character-by-character with the reference below.
  • Telegram bot hasn't received /start yet. Go back to your bot in Telegram and send /start if you haven't already.

Need help? Why is my GitHub Actions workflow failing?

✔️ Awesome, I've got everything!

Great. Double check you have saved .github/workflows/daily-digest.yml and confirmed the Telegram message arrived.

ⓧ I'd like to double check the full code

Your workflow file should look exactly like this:

name: AI News Buddy Daily Digest

on:
  schedule:
    - cron: '0 5 * * *'
  workflow_dispatch:

jobs:
  send-digest:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Run news buddy
        env:
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
        run: python news_buddy.py

Your Python files (config.py, news_buddy.py) and requirements.txt remain unchanged from the previous step.

What about the daily schedule?

The cron schedule '0 5 * * *' means your workflow runs automatically at 5:00 AM UTC every day. GitHub may delay scheduled runs by up to 15-30 minutes during peak load, but your morning digest will arrive consistently.

One thing to note: GitHub automatically disables scheduled workflows after 60 days of no repository activity on public repos. Push a small commit occasionally to keep it alive.

Your AI News Buddy is now fully hands-free. Every morning, GitHub Actions wakes up, runs your script, and you get a casual news digest on Telegram without lifting a finger.

Secret mission

Build the Overflow Queue

Some days the AI news firehose produces 30+ stories. Instead of losing the overflow, you save it for tomorrow. This gives your buddy a "memory" across days.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely on free services, so there are no ongoing costs.

Resources you used:

  • GitHub repository (public, free).
  • Gemini API key (free tier, 1,500 requests/day).
  • Telegram bot (free).
  • Local project folder with virtual environment.

Keep everything running

No action needed. Choose this if you want your daily digest to keep arriving.

  • Your GitHub Actions workflow continues running daily at no cost.
  • Public repositories disable scheduled workflows after 60 days of inactivity. Push a small commit occasionally to keep it active.
  • Your Gemini API key stays on the free tier with no charges.
  • Your Telegram bot remains active and free.

Pause - I'll come back to this later

Disable the automated workflow so it stops running, but keep all your code and credentials in place for later.

  • Go to your repository on GitHub.
  • Click the Actions tab.
  • In the left sidebar, click AI News Buddy Daily Digest.
  • Click the ... menu (three dots), then select Disable workflow.
  • Your code, secrets, and bot all remain intact. Re-enable the workflow anytime from the same menu.

Delete - I don't want to use this again

Remove all project resources and start fresh.

Delete the GitHub repository
  • Go to your repository on GitHub.
  • Click Settings (under the repository name).
  • Scroll down to the Danger Zone section.
  • Click Delete this repository, acknowledge the warnings, type your repository name to confirm, then click the final delete button.
Revoke your Gemini API key
  • Open Google AI Studio at aistudio.google.com/app/apikey.
  • Find the API key you created for this project.
  • Click the trash icon next to it and confirm deletion.
Delete your Telegram bot
  • Open your chat with @BotFather in Telegram.
  • Send /deletebot.
  • Select your bot from the list and confirm. This cannot be undone.
Remove local project files
  • In Terminal, navigate to your Desktop and delete your project folder:
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your-project-folder-name"]]
  • On Windows, open File Explorer, navigate to your Desktop, and delete the project folder.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully automated, zero-cost personal news assistant that messages you every morning on Telegram with curated insights written in the voice of a knowledgeable friend.

You've learned how to:

  • Built a Python script that fetches RSS feeds from four specialized domains and structures the raw data for AI processing.
  • Crafted a persona prompt that transforms dry news into casual, friend-like Telegram messages using the Gemini API for intelligent summarization.
  • Deployed an automated daily pipeline on GitHub Actions that runs every morning at zero cost, delivering curated news via the Telegram Bot API.
  • (Secret Mission) Implemented an overflow queue that preserves excess stories across days, giving the bot "memory" between runs.

Ready to quiz yourself?