#!/usr/bin/env python3 """ client/daily_client.py Fetch a quote from the API and post it to a Microsoft Teams channel via a webhook. Behavior: - Ensures a `.env` file exists next to this script with sensible defaults (if missing). - Loads KEY=VALUE pairs from the `.env` into the environment (if not already set). - Requests a random quote from the API using a Bearer token. - Posts the quote to Teams as a MessageCard (title: "Daily Quote"). Exit codes: 0 = success 1 = no quote returned 2 = failed to fetch quote 3 = failed to post to Teams """ import os import sys import json import requests import argparse from pathlib import Path # Ensure a .env file exists next to this script and load variables from it. def ensure_env_file(env_path: str, defaults: dict): """Create an env file at env_path with defaults if it doesn't exist.""" p = Path(env_path) if not p.exists(): try: p.parent.mkdir(parents=True, exist_ok=True) with p.open('w', encoding='utf-8') as f: for k, v in defaults.items(): f.write(f"{k}={v}\n") print(f"Created env file: {env_path}") except Exception as e: print(f"Failed to create env file {env_path}: {e}") def load_env_file(env_path: str): """Load KEY=VALUE lines from env_path into os.environ if not already set. Supports simple lines, ignores blank lines and lines starting with '#'. """ p = Path(env_path) if not p.exists(): return try: with p.open('r', encoding='utf-8') as f: for raw in f: line = raw.strip() if not line or line.startswith('#'): continue if '=' not in line: continue k, v = line.split('=', 1) k = k.strip() v = v.strip().strip('"').strip("'") if k and k not in os.environ: os.environ[k] = v except Exception as e: print(f"Failed to read env file {env_path}: {e}") # Path to .env in the same folder as this script _env_path = os.path.join(os.path.dirname(__file__), '.env') _defaults = { 'SCHLOTER_API_URL': 'http://schloter.api.ping-mee.de/quotes/random', 'TEAMS_WEBHOOK_URL': 'https://outlook.office.com/webhook/REPLACE_WITH_YOUR_WEBHOOK', 'SCHLOTER_API_TOKEN': '?2%?fK+@%Y9wy!f6' } ensure_env_file(_env_path, _defaults) load_env_file(_env_path) # Where to fetch a quote from (API) API_URL = os.environ.get('SCHLOTER_API_URL', _defaults['SCHLOTER_API_URL']) # Microsoft Teams Incoming Webhook URL (Workflows / connectors) TEAMS_WEBHOOK_URL = os.environ.get('TEAMS_WEBHOOK_URL', _defaults['TEAMS_WEBHOOK_URL']) # Static token for the API (can be set via environment variable) API_TOKEN = os.environ.get('SCHLOTER_API_TOKEN', _defaults['SCHLOTER_API_TOKEN']) def get_quote(): """Fetch a quote from the API using a Bearer token. Returns the quote string or raises on HTTP error. """ headers = {"Authorization": f"Bearer {API_TOKEN}"} resp = requests.get(API_URL, headers=headers, timeout=10) resp.raise_for_status() data = resp.json() # API returns {'id': ..., 'quote': '...'} return data.get("quote") def send_teams_message(webhook_url, title, message, color="0076D7"): """ Sends a formatted message to a Microsoft Teams channel using a webhook as a MessageCard. Treats HTTP 200 and 202 as success and prints response details. """ headers = {"Content-Type": "application/json"} payload = { "@type": "MessageCard", "@context": "https://schema.org/extensions", "themeColor": color, "summary": title, "sections": [ { "activityTitle": f"**{title}**", "text": message } ] } try: response = requests.post(webhook_url, headers=headers, data=json.dumps(payload), timeout=10) # Microsoft Teams incoming webhooks may return 200 or 202 for success if response.status_code in (200, 202): text = response.text.strip() if text: print(f"✅ Message sent successfully! Response: {response.status_code} - {text}") else: print(f"✅ Message sent successfully! Response: {response.status_code}") return True else: print(f"❌ Failed to send message: {response.status_code} - {response.text}") return False except Exception as e: print(f"⚠️ Error sending message: {e}") return False def post_to_teams(quote): """Wrapper to post the daily quote as a MessageCard to Teams.""" title = "Daily Quote" return send_teams_message(TEAMS_WEBHOOK_URL, title=title, message=quote) if __name__ == "__main__": # Allow a debug/dry-run mode so we don't have to call the schloter API. p = argparse.ArgumentParser(description="Fetch quote (or use local debug quote) and post to Teams webhook") p.add_argument('--no-api', action='store_true', help='Do not call the schloter API; use a local quote instead') p.add_argument('--local-quote', help='Local quote text to use when --no-api is set') args = p.parse_args() no_api_env = os.environ.get('SCHLOTER_DEBUG_NO_API', '').lower() in ('1', 'true', 'yes') use_no_api = args.no_api or no_api_env if use_no_api: # Choose local quote from CLI, env, or a sensible default quote = args.local_quote or os.environ.get('SCHLOTER_LOCAL_QUOTE') or 'Debug quote: no API call performed.' print('DEBUG: Running without calling schloter API; using local quote.') else: try: quote = get_quote() except Exception as e: print(f"Failed to fetch quote from API: {e}") sys.exit(2) if not quote: print("No quote available") sys.exit(1) print("Quote to send:", quote) ok = post_to_teams(quote) if not ok: print("Failed to post quote to Teams") sys.exit(3) print("Quote successfully posted to Teams.") sys.exit(0)