Discord bot for soccer match information and fun utilities, powered by FotMob data.
- ποΈ Match schedules with venue information
- πΊ US TV provider information
- π League standings
- π Support for major leagues (MLS, NWSL, World Cup, Premier League, Champions League)
- β° Times displayed in configurable timezone (default: Pacific Time)
- π© Country flags for World Cup matches
- β½ Real-time goal notifications for World Cup matches
- π₯ Automatic Reddit replay clips from r/soccer
- π Post-match summaries with highlights
- β±οΈ Extra time and penalty shootout alerts
- π¦οΈ Weather - Current weather conditions with AQI data
- π Sports Scores - Live scores for NBA, MLB, NHL, NFL, F1
- π LatePass - URL repost tracking with scoring system
- πΌ PandaPing - Automated Dodgers home game win/loss announcements
- π² Dice rolling - Standard RPG dice notation
- π± Magic 8-ball - Answer your burning questions
- Python 3.13+
uv(https://docs.astral.sh/uv/)- A Discord bot token
# Install dependencies
uv sync
# Install pre-commit hooks (optional but recommended)
uv tool install pre-commit
pre-commit install
# Create a .env file from the example
cp .env.example .env
# Edit .env and add your DISCORD_TOKEN
# Run the bot
uv run python run.pyThe bot uses a .env file for sensitive credentials. Create it in the project root:
# .env
DISCORD_TOKEN=your_discord_token_hereImportant: The .env file is already in .gitignore to prevent accidentally committing secrets to version control.
The bot uses config.json for settings. Create it in the project root:
{
"timezone": "America/Los_Angeles",
"log_level": "INFO",
"match_output_path": null,
"latepass": {
"ignored_domains": [
"tenor.com",
"giphy.com",
"gfycat.com",
"imgur.com"
]
},
"channel_leagues": {
"mls": "mls",
"nwsl": "nwsl",
"premier-league": "premier_league",
"ucl": "champions_league",
"world-cup-2026": "world_cup"
},
"world_cup": {
"enabled": true,
"channel_name": "world-cup-2026",
"daily_time_hour": 8,
"live_monitoring": {
"enabled": true,
"channel_name": "world-cup-2026-live",
"check_interval_seconds": 60,
"pre_match_minutes": 15,
"fallback_check_hours": 12,
"notifications": {
"goals": true,
"cards": true,
"substitutions": true,
"half_events": true,
"extra_time": true,
"penalties": true,
"include_reddit_clips": true
}
},
"highlights": {
"reddit_enabled": true,
"cache_path": "~/.lafcbot/reddit_cache.json"
}
},
"pandaping": {
"servers": [
{
"guild_id": "YOUR_GUILD_ID",
"channel_name": "other-sports",
"role_name": "Panda Ping",
"announce_wins": true,
"announce_losses": true,
"daily_reminder": true
}
]
}
}Configure which league !matches shows by default in each channel. When !matches is called without arguments in a mapped channel, it shows that league's matches automatically.
Valid league identifiers:
mls- Major League Soccernwsl- National Women's Soccer Leaguepremier_league- English Premier Leaguechampions_league- UEFA Champions Leagueworld_cup- FIFA World Cup
Configuration formats:
Single-server (legacy format): Maps channel names directly to leagues
"channel_leagues": {
"mls": "mls",
"premier-league": "premier_league",
"ucl": "champions_league"
}Multi-server format: Maps guild_id to channel-league mappings
"channel_leagues": {
"123456789012345678": {
"mls": "mls",
"premier-league": "premier_league"
},
"987654321098765432": {
"soccer": "champions_league",
"football": "world_cup"
}
}Both formats are supported. The bot will check guild-specific mappings first, then fall back to global mappings. In unmapped channels, it defaults to MLS.
To get your guild ID: Enable Developer Mode in Discord settings, right-click server name, "Copy Server ID"
timezone: IANA timezone name (e.g., "America/Los_Angeles", "America/New_York")- Used for time displays across the bot (matches, weather, latepass timestamps)
- Defaults to "America/Los_Angeles" if not specified
-
log_level: Controls logging verbosity (optional)- Valid values:
"DEBUG","INFO","WARNING","ERROR" - Default:
"INFO"if not specified - Use
"DEBUG"for troubleshooting issues
- Valid values:
-
match_output_path: Directory path for match data JSON dumps (optional)- Set to a directory path (e.g.,
"test_data") to save raw match data from FotMob - Set to
nullor omit to disable (default behavior) - Useful for debugging match parsing issues or analyzing FotMob's data structure
- Files are saved as
match_{match_id}_dump.jsonin the specified directory - Directory is created automatically if it doesn't exist
- Set to a directory path (e.g.,
latepass.ignored_domains: Array of domain names to ignore for URL tracking- Useful for excluding GIF/image hosts that are frequently "reposted" but are just media links
- Supports subdomain matching (e.g., "tenor.com" matches "media.tenor.com")
- Default domains to consider:
tenor.com,giphy.com,gfycat.com,imgur.com
Configuration Format:
The World Cup feature supports both legacy single-server and new multi-server configurations.
Multi-server format (recommended):
"world_cup": {
"enabled": true,
"daily_time_hour": 8,
"servers": [
{
"guild_id": "123456789012345678",
"channel_name": "world-cup-2026",
"live_channel_name": "world-cup-2026-live"
},
{
"guild_id": "987654321098765432",
"channel_name": "wc-updates",
"live_channel_name": "wc-live"
}
],
"live_monitoring": { ... }
}Legacy single-server format (still supported):
"world_cup": {
"enabled": true,
"channel_name": "world-cup-2026",
"live_monitoring": {
"channel_name": "world-cup-2026-live",
...
}
}Basic Settings:
enabled: Toggle all World Cup features on/offdaily_time_hour: Hour (0-23) for daily schedule postsservers: Array of server configurations (multi-server format)guild_id: Discord server/guild ID (required) - Enable Developer Mode, right-click server, "Copy Server ID"channel_name: Channel for daily spoiler-free scheduleslive_channel_name: Channel for live match updates
Live Monitoring:
live_monitoring.enabled: Enable real-time goal notifications and live match monitoringlive_monitoring.check_interval_seconds: How often to check for updates during matches (default: 60)live_monitoring.pre_match_minutes: Start monitoring N minutes before kickoff (default: 15)live_monitoring.fallback_check_hours: How long to sleep if no matches found (default: 12)
Notifications:
notifications.goals: Enable goal notificationsnotifications.cards: Enable yellow/red card notificationsnotifications.substitutions: Enable player substitution notificationsnotifications.half_events: Enable half-time and full-time notificationsnotifications.extra_time: Enable extra time alertsnotifications.penalties: Enable penalty shootout alertsnotifications.include_reddit_clips: Automatically fetch Reddit replay clips for goals
Highlights:
highlights.reddit_enabled: Enable Reddit r/soccer clip searchinghighlights.cache_path: Where to cache found clips
If config.json is missing, the bot will run with World Cup updates disabled.
Configure Dodgers game announcements per server:
Configuration options:
servers: Array of server configurations (required)guild_id: Discord server/guild ID (required) - Enable Developer Mode in Discord, right-click server name, "Copy Server ID"channel_name: Channel name for announcements (default: "other-sports")role_name: Role to mention (default: "Panda Ping")announce_wins: Send notifications for wins (default: true)announce_losses: Send notifications for losses (default: true)daily_reminder: Send daily reminders at 10 AM (default: true)
Example configuration:
"pandaping": {
"servers": [
{
"guild_id": "123456789012345678",
"channel_name": "other-sports",
"role_name": "Panda Ping",
"announce_wins": true,
"announce_losses": true,
"daily_reminder": true
},
{
"guild_id": "987654321098765432",
"channel_name": "dodgers",
"role_name": "Baseball Fans",
"announce_wins": true,
"announce_losses": false,
"daily_reminder": false
}
]
}Multi-server support: You can configure PandaPing for multiple Discord servers by adding multiple entries to the servers array. Each server can have different channel names, role names, and notification preferences.
Shows matches for the current day, or the next day with matches if none today.
If no league is specified, the command uses the league configured for the current Discord channel (see Configuration below). If no channel mapping exists, defaults to MLS.
Examples:
!matches # Uses channel-configured league (or MLS if none)
!matches World Cup # World Cup with country flags
!matches Premier # Premier League
!matches UCL # Champions League
Output includes:
- β Finished matches with scores
- π΄ Live matches in progress
- Match times in Pacific Time
- ποΈ Venue/stadium information (first 5 upcoming matches)
- πΊ US TV providers (when available)
- π© Country flags (World Cup only)
Sample:
**World Cup Matches**
**Today's Matches:**
β
πΊπΈ USA 2-1 π΅πΎ Paraguay
π΄ π§π· Brazil 1-0 π²π¦ Morocco
**Upcoming:**
π²π½ Mexico vs πΏπ¦ South Africa - Jun 11, 12:00 PM PT
ποΈ Mexico City Stadium, Ciudad de MΓ©xico
πΊ FOX, Telemundo
Shows league standings/tables.
If no league is specified, the command uses the league configured for the current Discord channel (see Configuration below). If no channel mapping exists, defaults to MLS.
Examples:
!standings # Uses channel-configured league (or MLS if none)
!standings World Cup # World Cup group standings
!standings Premier # Premier League table
Output:
- Top 10 teams per table
- Position, Team, Played, Wins, Draws, Losses, Goal Difference, Points
- Multiple tables for leagues with conferences (e.g., MLS Eastern/Western)
Shows detailed match summary with goals, assists, and highlights.
Examples:
!match 4193490
Output includes:
- Match status (Live, Finished, or Upcoming)
- Final score or current score
- All goals with scorer and assist information
- π₯ Official match highlights link (if available)
- ποΈ Venue information
- Penalty shootout results (if applicable)
Shows top player statistics for a league.
If no league is specified, the command uses the league configured for the current Discord channel. If no stat type is specified, defaults to goals.
Examples:
!stats # Top goals for channel-configured league
!stats goals # Same as above
!stats assists # Top assists for channel-configured league
!stats goals wc # World Cup top scorers
!stats assists mls # MLS top assists
!stats assist world cup # World Cup top assists (singular also works)
Stat types: goals, assists (or goal, assist)
Output includes:
- Player name
- Team name
- Statistic count
- Top performers (typically top 5)
Shows current weather conditions with detailed information.
Examples:
!weather # Uses your saved location
!weather Seattle # Weather for Seattle
!weather 90210 # Weather by ZIP code
Output includes:
- Current temperature (F/C) and conditions
- Feels like temperature
- Humidity percentage
- Wind speed and direction
- Air Quality Index (AQI) with category
- Today's high/low and precipitation chance
Sample:
Seattle, Washington, United States: 58F overcast; feels 56F; humidity 81%; wind ESE 5 mph; AQI 48 good; Today 59F/49F, 91% rain
The bot remembers your last location for quick checks.
Shows today's scores for a sports league in a single concise line.
Examples:
!scores # Show available leagues
!scores mlb # Major League Baseball: SEA 6 @ BAL 3 Final | NYY 7 @ CLE 5 Bot 10th | ...
!scores nba # National Basketball Association: SA 76 @ NY 76 5:37 - 3rd
!scores nhl # National Hockey League: VGK @ CAR (Mon 6/9 8:00 PM)
!scores nfl # National Football League: SEA @ NE (Sun 9/9 8:20 PM)
!scores f1 # Formula 1: [race results]
Supported leagues: nba, mlb, nhl, nfl, f1
Output includes:
- Live games with current score and clock time
- Final games with final scores
- Scheduled games with localized start time
- All games on a single line separated by
|
LatePass automatically tracks URL reposts and maintains a scoring system. See LATEPASS.md for complete documentation.
Shows your latepass score or another user's score.
Examples:
!latepass # Your score
!latepass @Alice # Alice's score
Shows the server leaderboard (top 10 by default, range 5-50).
Shows server-wide statistics (total URLs, reposts, most reposted URL).
Shows most reposted URLs (top 10 by default, range 5-25).
Shows viral URLs with high repost counts (10+ by default, range 5-100).
Scoring System:
- Original poster: +1 point per repost
- Reposter: -1 point per repost
- Positive scores = sharing fresh content
- Negative scores = reposting others' content
Auto-tracking: When someone reposts a URL, the bot automatically:
- Adds a :LatePass: emoji reaction
- Replies with original poster, time ago, scores, ranks, and total reposts
- Updates both users' scores
PandaPing automatically monitors Dodgers home games and announces results to the #other-sports channel with role mentions.
Check if PandaPing is enabled and active.
Shows current monitoring status, active game info, and next scheduled game.
Example output:
πΌ PandaPing Status:
Monitoring: Active π’
Current Game: Dodgers vs Giants - Top 5th, LAD 3-2
Next Game: Tomorrow at 7:10 PM PT vs Padres (Home)
Manually trigger a check for Dodgers game updates (useful for testing or forcing an update).
Automatic Features:
- Game Result Announcements: When a Dodgers home game ends, automatically posts to #other-sports
- Wins: Mentions @Panda Ping role with final score
- Losses: Posts without role mention (still visible but no notification)
- All results use spoiler tags to hide scores
- Daily Reminders: Every day around 10 AM PT (with randomization), posts upcoming home games for the day
- Smart Monitoring: Only actively checks during game times to minimize API usage
!ping- Check bot latency!wut- Just... wut!dice <notation>- Roll dice (e.g.,!dice 3d6+2)!8ball <question>- Ask the magic 8-ball!servers- (Owner only) List servers bot is in
| League | Aliases | ID |
|---|---|---|
| MLS | mls, Major League Soccer | 130 |
| NWSL | nwsl, womens | 289 |
| World Cup | World Cup, world cup, WC, worldcup | 77 |
| Premier League | Premier, EPL, English Premier League | 47 |
| Champions League | Champions, UCL, UEFA | 42 |
All commands are case-insensitive and support multi-word names without quotes.
The bot includes a complete async Python library for scraping FotMob:
- Scraping: Extracts
__NEXT_DATA__JSON from Next.js SSR pages - Rate Limiting: 1 second delay between requests
- Retry Logic: 3 attempts with exponential backoff
- Data Models: Type-safe dataclasses for matches, teams, venues, standings
Location: fotmob/ package
- Soccer Match Data: FotMob.com (via HTML scraping and API)
- Sports Scores: ESPN public scoreboard API (NBA, MLB, NHL, NFL, F1)
- Venue Information: Extracted from match details pages
- TV Providers: Extracted from match page HTML (US only)
- Goal Replay Clips: Reddit r/soccer (direct JSON API)
- Highlights: FotMob official highlights URLs
- Weather Data: Open-Meteo API (free, no API key required)
- Air Quality: Open-Meteo Air Quality API
The bot attempts to fetch goal replay clips from Reddit's r/soccer community using the public JSON API.
How it works:
- Uses Reddit's public JSON endpoint (
/r/soccer/search.json) - Searches with time filtering (Β±12 hours from match time)
- Filters by "Media" flair
- Due to Reddit's bot detection, may occasionally return 403 (Forbidden) errors
- Results are cached for 24 hours to minimize API calls
No configuration needed - clips are fetched automatically when available.
The bot uses SQLite for persistent storage:
Tables:
user- User preferences (weather locations)posted_urls- First post tracking for each URL per guildlatepass_score- User scores and rankings per guild
Database is automatically created at lafcbot.db on first run.
Times are displayed in the configured timezone (from config.json), defaulting to Pacific Time (PT). Automatically handles PST/PDT transitions. Used for:
- Match times and "today" filtering
- Weather latepass timestamps
- LatePass relative time displays
lafcbot/
βββ lafcbot/ # Main package
β βββ __init__.py # Package exports
β βββ bot.py # Discord bot setup and core commands
β βββ db.py # Database operations (SQLite)
β βββ cogs/ # Discord command cogs
β β βββ __init__.py
β β βββ soccer.py # Soccer commands (matches, standings, match, stats)
β β βββ latepass.py # LatePass URL tracking system
β β βββ pandaping.py # PandaPing Dodgers announcements
β β βββ misc.py # Utility commands (weather, scores, dice, 8ball, wut)
β βββ tasks/ # Background tasks
β β βββ __init__.py
β β βββ world_cup.py # World Cup daily schedule + live monitoring
β βββ match_events/ # Match event monitoring system
β β βββ __init__.py
β β βββ monitor.py # Live match monitoring loop
β β βββ tracker.py # Event state tracking
β β βββ detectors.py # Event detection logic
β β βββ formatters.py # Event message formatting helpers
β β βββ notifiers.py # Discord notification formatting
β βββ formatters/ # Message formatters (testable, reusable)
β β βββ __init__.py
β β βββ base.py # BaseFormatter with shared utilities
β β βββ world_cup.py # World Cup daily match notifications
β β βββ soccer.py # Soccer command responses
β β βββ latepass.py # LatePass command responses
β β βββ sports.py # Sports scores (ESPN API)
β β βββ weather.py # Weather command responses
β β βββ misc.py # Misc commands (dice, 8ball)
β βββ utils/ # Shared utilities
β β βββ __init__.py
β β βββ config.py # Configuration loading
β β βββ checks.py # Discord command decorators
β β βββ errors.py # Error handling decorators
β β βββ time.py # Time formatting utilities
β β βββ countries.py # Country flag emoji mapping
β β βββ discord_helpers.py # Discord-specific helpers
β βββ clients/ # API clients
β βββ __init__.py
β βββ fotmob/ # FotMob wrapper library
β β βββ client.py # HTTP client with rate limiting
β β βββ constants.py # League IDs and configuration
β β βββ models.py # Data models (Match, MatchEvent, Highlight, etc.)
β β βββ parser.py # HTML/JSON extraction
β β βββ __init__.py # Public API
β βββ espn_client.py # ESPN API for sports scores
β βββ reddit_client.py # Reddit r/soccer clip fetcher with caching
β βββ open_meteo_client.py # Open-Meteo weather API client
βββ tests/ # Unit tests
β βββ formatters/ # Formatter tests (46 tests)
βββ run.py # Entry point
βββ config.json # Bot configuration (user-created)
βββ lafcbot.db # SQLite database (auto-created)
βββ LATEPASS.md # LatePass system documentation
βββ WORLD_CUP_FEATURES.md # Detailed World Cup features documentation
βββ .pre-commit-config.yaml # Pre-commit hook configuration
βββ ruff.toml # Ruff linting rules
βββ pyproject.toml # Dependencies and metadata
βββ README.md # This file
py-cord>=2.0- Discord bot frameworkaiohttp>=3.9.0- Async HTTP client (for FotMob, Reddit, and weather APIs)beautifulsoup4>=4.12.0- HTML parsinglxml>=5.0.0- Fast XML/HTML processingaiosqlite>=0.20.0- Async SQLite database (for user preferences and latepass tracking)
This project uses Ruff for linting and formatting. Pre-commit hooks automatically run Ruff on all staged files.
# Install pre-commit hooks
uv tool install pre-commit
pre-commit install
# Run manually on all files
pre-commit run --all-files
# Run ruff directly
uv tool run ruff check .
uv tool run ruff format .Configuration:
.pre-commit-config.yaml- Pre-commit hook configurationruff.toml- Ruff linting and formatting rules
Intended for:
- Educational purposes
- Personal use
- Non-commercial projects
Not for:
- Commercial applications
- High-volume scraping
- Redistributing FotMob's data
The bot respects FotMob's servers:
- 1 second delay between requests
- Browser-like User-Agent headers
- Exponential backoff on failures
- Dependent on FotMob's HTML structure
- May break if FotMob updates their website
- FotMob's old API endpoints are deprecated (return 404)
For detailed information about World Cup live monitoring features, see WORLD_CUP_FEATURES.md.
When live monitoring is enabled, the bot uses smart scheduling to minimize API calls while ensuring timely notifications:
- Intelligent scheduling: Only polls the API when matches are happening
- Pre-match activation: Starts monitoring 15 minutes before kickoff (configurable)
- Auto-sleep: When no matches are scheduled, sleeps for hours instead of constant polling
- 95% fewer API calls on non-match days while maintaining full functionality
During matches, the bot will:
- Monitor live World Cup matches every 60 seconds
- Send instant notifications when goals are scored, including:
- Scorer and assist
- Current score
- Country flags
- Automatic Reddit replay clips (when available)
- Alert on special events:
- Extra time notifications
- Penalty shootout alerts
- Post automatic summaries when matches finish with:
- All goals and assists
- Official FotMob highlights
- Final score and penalty results
Goal:
β½ GOAL! πͺπΈ Spain 2-1 π©πͺ Germany
Scorer: Γlvaro Morata 67'
Assist: Dani Olmo
π₯ Replay
Post-Match:
π FINAL: πͺπΈ Spain 3-1 π©πͺ Germany
β½ Goals:
12' - Pedri
34' - Morata (Olmo)
67' - Morata
πΊ Official Highlights: [Watch](...)
- Goal notification system inspired by golazo project
- Data from FotMob.com and Reddit r/soccer
- Built with py-cord