This commit is contained in:
2026-04-23 10:23:50 +02:00
parent 8481a1b6f1
commit df36516193
2 changed files with 217 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
"""
EmailMessagingGraph.py
----------------------
Private Microsoft Graph mail sender
Application permissions, shared mailbox
"""
import base64
import msal
import requests
from functools import lru_cache
from pathlib import Path
from typing import Union, List
# =========================
# PRIVATE CONFIG (ONLY YOU)
# =========================
TENANT_ID = "7d269944-37a4-43a1-8140-c7517dc426e9"
CLIENT_ID = "4b222bfd-78c9-4239-a53f-43006b3ed07f"
CLIENT_SECRET = "Txg8Q~MjhocuopxsJyJBhPmDfMxZ2r5WpTFj1dfk"
SENDER = "reports@buzalka.cz"
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
SCOPE = ["https://graph.microsoft.com/.default"]
@lru_cache(maxsize=1)
def _get_token() -> str:
app = msal.ConfidentialClientApplication(
CLIENT_ID,
authority=AUTHORITY,
client_credential=CLIENT_SECRET,
)
token = app.acquire_token_for_client(scopes=SCOPE)
if "access_token" not in token:
raise RuntimeError(f"Graph auth failed: {token}")
return token["access_token"]
def send_mail(
to: Union[str, List[str]],
subject: str,
body: str = "",
*,
html: bool = False,
attachments: Union[str, Path, List[Union[str, Path]], None] = None,
):
"""
Send email via Microsoft Graph.
:param to: email or list of emails
:param subject: subject
:param body: email body (default empty)
:param html: True = HTML, False = plain text
:param attachments: file path or list of file paths to attach
"""
if isinstance(to, str):
to = [to]
if attachments is None:
attachments = []
elif isinstance(attachments, (str, Path)):
attachments = [attachments]
attachment_payloads = []
for path in attachments:
path = Path(path)
attachment_payloads.append({
"@odata.type": "#microsoft.graph.fileAttachment",
"name": path.name,
"contentType": "application/octet-stream",
"contentBytes": base64.b64encode(path.read_bytes()).decode(),
})
payload = {
"message": {
"subject": subject,
"body": {
"contentType": "HTML" if html else "Text",
"content": body,
},
"toRecipients": [
{"emailAddress": {"address": addr}} for addr in to
],
**({"attachments": attachment_payloads} if attachment_payloads else {}),
},
"saveToSentItems": "true",
}
headers = {
"Authorization": f"Bearer {_get_token()}",
"Content-Type": "application/json",
}
r = requests.post(
f"https://graph.microsoft.com/v1.0/users/{SENDER}/sendMail",
headers=headers,
json=payload,
timeout=30,
)
if r.status_code != 202:
raise RuntimeError(
f"sendMail failed [{r.status_code}]: {r.text}"
)
@@ -0,0 +1,106 @@
"""
Stáhne daily Str8ts puzzle jako PDF ze solitaire.org a uloží do stejné složky.
Název souboru: yyyy-mm-dd Daily Str8ts puzzle.pdf
"""
import asyncio
import sys
from datetime import date
from pathlib import Path
from playwright.async_api import async_playwright
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "Knihovny"))
from EmailMessagingGraph import send_mail
OUTPUT_DIR = Path(__file__).parent
URL = "https://www.solitaire.org/daily-str8ts/"
RECIPIENT = ["vladimir.buzalka@buzalka.cz", "alica.buzalkova@buzalka.cz"]
EMAIL_BODY = """Str8ts — pravidla
Hrací pole 9×9, každá buňka je buď bílá nebo černá.
1. Číslice 19 — do bílých buněk piš čísla 19.
2. Žádné opakování v řádku/sloupci — stejné číslo se nesmí opakovat v celém řádku ani sloupci (jako Sudoku).
3. Straights (sekvence) — bílé buňky oddělené černými tvoří skupiny. Čísla v každé skupině musí tvořit sadu po sobě jdoucích čísel (v libovolném pořadí). Např. skupinka tří buněk může obsahovat {3,4,5} nebo {7,8,9}, ale ne {1,3,5}.
4. Délka sekvence — skupina o délce n musí obsahovat právě n různých čísel jdoucích za sebou.
5. Černé buňky s číslem — někdy mají předvyplněné číslo jako nápovědu; toto číslo se nepočítá do sekvencí, ale blokuje opakování v řádku/sloupci.
Rozdíl od Sudoku: nemusíš vyplnit 19 do každé skupiny — jen zajistit, že čísla v každé skupině tvoří „straight" (jako v pokeru).
"""
async def main():
today = date.today().strftime("%Y-%m-%d")
output_path = OUTPUT_DIR / f"{today} Daily Str8ts puzzle.pdf"
if output_path.exists():
print(f"Soubor již existuje: {output_path}")
return
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(
viewport={"width": 1280, "height": 900},
)
page = await context.new_page()
print(f"Načítám {URL} ...")
await page.goto(URL, wait_until="networkidle", timeout=60_000)
# Najdi iframe s hrou (solitaire.org vkládá hru do iframe)
game_frame = None
for frame in page.frames:
if frame.url != page.url and frame.url.strip() not in ("", "about:blank"):
game_frame = frame
print(f" Nalezen iframe: {frame.url}")
break
target = game_frame if game_frame else page
# Zkus kliknout na Print tlačítko (různé možné selektory)
print_selectors = [
"text=Print",
"button:has-text('Print')",
"[title*='print' i]",
"[aria-label*='print' i]",
".print-button",
"#print",
]
clicked = False
for sel in print_selectors:
try:
await target.click(sel, timeout=3_000)
clicked = True
print(f" Kliknuto na Print ({sel})")
await page.wait_for_timeout(1_500)
break
except Exception:
pass
if not clicked:
print(" Tlačítko Print nenalezeno — ukládám celou stránku jako PDF.")
# Uložit jako PDF
await page.pdf(
path=str(output_path),
format="A4",
print_background=True,
margin={"top": "10mm", "bottom": "10mm", "left": "10mm", "right": "10mm"},
)
print(f"PDF uloženo: {output_path}")
await browser.close()
send_mail(
to=RECIPIENT,
subject="Posílám dnešní Str8ts puzzle v příloze",
body=EMAIL_BODY,
attachments=output_path,
)
print(f"Email odeslán na {RECIPIENT}")
if __name__ == "__main__":
asyncio.run(main())