Automatically collects defence and sovereignty news from Canadian government feeds, think tanks, major media, and Google News — then delivers a clean, deduplicated digest to Microsoft Teams twice a day.
- Overview
- How It Works
- Features
- Quick Start
- Automated Deployment
- Command-Line Usage
- Configuration
- Project Structure
- License
NewsBot scans dozens of Canadian sources for news about defence, security, and sovereignty, scores each article for relevance, removes anything you've already seen, and posts the rest to a Microsoft Teams channel as a polished Adaptive Card.
It runs entirely on free infrastructure — GitHub Actions for scheduling and a Teams Workflows webhook for delivery — so there are no servers to maintain and no paid services required.
The pipeline runs in four stages:
flowchart LR
A[Collect feeds] --> B[Filter by relevance]
B --> C[Deduplicate]
C --> D[Send to Teams]
| Stage | Module | What it does |
|---|---|---|
| 1. Collect | feed_collector.py |
Fetches and parses every configured RSS/Atom feed and Google News query, normalizing them into Article objects. |
| 2. Filter | keyword_filter.py |
Scores each article with a three-layer relevance model (see Tuning Keywords). |
| 3. Deduplicate | dedup.py |
Tracks every sent article in SQLite so nothing is ever delivered twice. |
| 4. Send | teams_sender.py |
Formats the survivors into a grouped Adaptive Card and POSTs it to the Teams webhook. |
main.py orchestrates the stages and provides the command-line interface.
- Multi-source collection — Canadian government feeds (DND, Global Affairs, NSERC, CSA, IDEaS, ISED, and more), think tanks (CDA Institute, Macdonald-Laurier, NAADSN, CIC), major media (CBC, CTV, Global News, National Post, Globe and Mail), 18 targeted Google News queries, and optional LinkedIn feeds via RSS.app.
- Smart relevance filtering — A three-layer scoring model (primary topic → Canada relevance → domain context) with negative-keyword exclusion, so you get defence news, not hockey "defence."
- Reliable deduplication — SQLite-backed tracking by URL hash, plus cross-source title matching to collapse the same story found by multiple queries.
- Clean Teams delivery — Articles grouped by source type in a numbered, clickable Adaptive Card, posted through a free Teams Workflows webhook.
- Hands-off automation — GitHub Actions runs the bot every 12 hours; no servers required.
- Fully configurable — All sources, keywords, and scoring thresholds live in plain YAML.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txtcp .env.example .env
# Edit .env and paste your Teams webhook URLTo create a Teams webhook (free, no premium plan required):
- In Teams, click ⋯ next to the target channel → Workflows.
- Choose "Post to a channel when a webhook request is received."
- Name it (e.g. NewsBot) and finish the prompts.
- Copy the generated webhook URL into
.envasTEAMS_WEBHOOK_URL.
python -m src.main --dry-runpython -m src.mainThe included workflow (.github/workflows/newsbot.yml) runs the bot on a schedule
with zero infrastructure.
-
Push the repository to GitHub (already done if you're reading this there).
-
Add your webhook as a secret: repo Settings → Secrets and variables → Actions → New repository secret.
- Name:
TEAMS_WEBHOOK_URL - Value: your full webhook URL
- Name:
-
Done. The workflow runs automatically twice a day:
Schedule UTC (cron) 9:00 AM EST 0 14 * * *9:00 PM EST 0 2 * * *
You can also trigger it on demand from the Actions tab → NewsBot → Run workflow.
Note: Cron times are fixed to UTC, so the local clock time shifts by an hour during daylight saving. Adjust the cron expressions in the workflow if you need exact local times year-round.
python -m src.main [options]
Options:
--dry-run Collect and preview articles without sending to Teams
--schedule [HH:MM] Run continuously, firing daily at HH:MM (default: 07:00)
--max-age HOURS Maximum article age to consider, in hours (default: 48)
--verbose, -v Enable debug logging
--stats Show deduplication database statistics
Examples:
python -m src.main --dry-run # Preview today's digest
python -m src.main # Collect and send to Teams
python -m src.main --schedule 08:00 # Run daily at 8:00 AM (local time)
python -m src.main --stats # Inspect the dedup databaseEdit config/sources.yaml. Each source category is a simple list — append or
remove entries as needed:
government— Canada.ca, DND, NSERC, Global Affairs, CSA, IDEaS, ISED, …think_tanks— CDA Institute, Macdonald-Laurier, NAADSN, CICmedia— CBC (Politics / Canada / Tech), CTV, Global News, National Post, Globe and Mailgoogle_news_queries— keyword searches covering defence research, the Arctic, quantum, procurement, NORAD, and morelinkedin_rss(optional) — RSS.app feeds for LinkedIn pages
Edit config/keywords.yaml. The filter applies a three-layer model:
primary_keywords— the topics you care about; an article must match at least one.canada_keywords— Canada relevance check, so non-trusted sources can't slip in foreign defence news.context_keywords— domain validation that proves the article is about defence, not an unrelated mention.
Trusted source categories (trusted_categories) skip layers 2 and 3.
negative_keywords instantly disqualify false positives (sports, entertainment, etc.),
and the scoring block controls title weighting and the minimum score per source tier.
NewsBot/
├── .github/workflows/
│ └── newsbot.yml # GitHub Actions schedule (every 12 hours)
├── config/
│ ├── sources.yaml # RSS feed URLs and Google News queries
│ └── keywords.yaml # Keywords, context validation, and scoring
├── src/
│ ├── main.py # CLI, orchestrator, and scheduler
│ ├── feed_collector.py # RSS/Atom fetching and parsing
│ ├── keyword_filter.py # Three-layer relevance scoring
│ ├── dedup.py # SQLite duplicate tracking
│ └── teams_sender.py # Teams Adaptive Card formatting
├── data/ # Auto-created DB + logs (git-ignored)
├── .env.example # Template for your .env
├── requirements.txt
└── README.md
Released under the MIT License.