Users can now choose between CheapShark, ITAD, or both via the DEAL_SOURCES env var (comma-separated, default: "cheapshark"). When "itad" is included, the bot polls GET /deals/v2 for current deals with discount/price filtering and posts them with the same formatting style. ITAD deals include built-in historical low detection via the API's deal flags. Preflight checks enforce ITAD_API_KEY when ITAD is a configured deal source. https://claude.ai/code/session_01B7YPGrE3NatkwadCXVjv2j
176 lines
4.8 KiB
Python
176 lines
4.8 KiB
Python
"""Message formatting for Matrix deal posts.
|
|
|
|
Produces both a plain-text fallback and HTML formatted body for Matrix messages.
|
|
"""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
import markdown
|
|
|
|
from .cheapshark import CheapSharkDeal
|
|
from .currency import format_price
|
|
from .epic import EpicFreeGame
|
|
from .itad_deals import ITADDeal
|
|
|
|
_md = markdown.Markdown()
|
|
|
|
|
|
def _render_html(md_text: str) -> str:
|
|
"""Convert Markdown to HTML, resetting the parser between calls."""
|
|
_md.reset()
|
|
return _md.convert(md_text)
|
|
|
|
|
|
def format_deal(deal: CheapSharkDeal, is_historical_low: bool = False) -> tuple[str, str]:
|
|
"""Format a CheapShark deal into (plain_text, html) for Matrix.
|
|
|
|
Returns (body, formatted_body).
|
|
"""
|
|
discount = int(deal.savings)
|
|
sale_price = float(deal.sale_price)
|
|
normal_price = float(deal.normal_price)
|
|
|
|
sale_multi = format_price(sale_price)
|
|
normal_display = format_price(normal_price)
|
|
|
|
lines = [
|
|
f"**🎮 [DEAL] {deal.title}**",
|
|
f"> {discount}% off on {deal.store_name} ~~{normal_display}~~",
|
|
f"> 💰 **{sale_multi}**",
|
|
]
|
|
|
|
if is_historical_low:
|
|
lines.append("> 🏆 _All-time low!_")
|
|
|
|
lines.append(f"> 🔗 [View Deal]({deal.deal_url})")
|
|
|
|
md_text = "\n".join(lines)
|
|
html = _render_html(md_text)
|
|
|
|
# Plain text fallback — strip markdown syntax
|
|
plain_lines = [
|
|
f"🎮 [DEAL] {deal.title}",
|
|
f" {discount}% off on {deal.store_name} (was {normal_display})",
|
|
f" 💰 {sale_multi}",
|
|
]
|
|
if is_historical_low:
|
|
plain_lines.append(" 🏆 All-time low!")
|
|
plain_lines.append(f" 🔗 {deal.deal_url}")
|
|
|
|
plain_text = "\n".join(plain_lines)
|
|
|
|
return plain_text, html
|
|
|
|
|
|
def format_itad_deal(deal: ITADDeal) -> tuple[str, str]:
|
|
"""Format an ITAD deal into (plain_text, html) for Matrix.
|
|
|
|
Returns (body, formatted_body).
|
|
"""
|
|
sale_multi = format_price(deal.sale_price)
|
|
normal_display = format_price(deal.normal_price)
|
|
|
|
lines = [
|
|
f"**🎮 [DEAL] {deal.title}**",
|
|
f"> {deal.discount}% off on {deal.shop_name} ~~{normal_display}~~",
|
|
f"> 💰 **{sale_multi}**",
|
|
]
|
|
|
|
if deal.is_historical_low:
|
|
lines.append("> 🏆 _All-time low!_")
|
|
|
|
lines.append(f"> 🔗 [View Deal]({deal.url})")
|
|
|
|
md_text = "\n".join(lines)
|
|
html = _render_html(md_text)
|
|
|
|
plain_lines = [
|
|
f"🎮 [DEAL] {deal.title}",
|
|
f" {deal.discount}% off on {deal.shop_name} (was {normal_display})",
|
|
f" 💰 {sale_multi}",
|
|
]
|
|
if deal.is_historical_low:
|
|
plain_lines.append(" 🏆 All-time low!")
|
|
plain_lines.append(f" 🔗 {deal.url}")
|
|
|
|
plain_text = "\n".join(plain_lines)
|
|
|
|
return plain_text, html
|
|
|
|
|
|
def format_epic_free(game: EpicFreeGame) -> tuple[str, str]:
|
|
"""Format an Epic free game into (plain_text, html) for Matrix.
|
|
|
|
Returns (body, formatted_body).
|
|
"""
|
|
lines = [
|
|
f"**🆓 [FREE] {game.title}**",
|
|
"> Free on Epic Games Store",
|
|
]
|
|
|
|
if game.end_date:
|
|
try:
|
|
end_dt = datetime.fromisoformat(game.end_date.replace("Z", "+00:00"))
|
|
date_str = end_dt.strftime("%B %-d")
|
|
lines.append(f"> 📅 _Free until {date_str}_")
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
if game.url:
|
|
lines.append(f"> 🔗 [Claim Now]({game.url})")
|
|
|
|
md_text = "\n".join(lines)
|
|
html = _render_html(md_text)
|
|
|
|
# Plain text fallback
|
|
plain_lines = [
|
|
f"🆓 [FREE] {game.title}",
|
|
" Free on Epic Games Store",
|
|
]
|
|
if game.end_date:
|
|
try:
|
|
end_dt = datetime.fromisoformat(game.end_date.replace("Z", "+00:00"))
|
|
date_str = end_dt.strftime("%B %-d")
|
|
plain_lines.append(f" 📅 Free until {date_str}")
|
|
except (ValueError, TypeError):
|
|
pass
|
|
if game.url:
|
|
plain_lines.append(f" 🔗 {game.url}")
|
|
|
|
plain_text = "\n".join(plain_lines)
|
|
|
|
return plain_text, html
|
|
|
|
|
|
def format_epic_upcoming(game: EpicFreeGame) -> tuple[str, str]:
|
|
"""Format an upcoming Epic free game into (plain_text, html) for Matrix."""
|
|
lines = [
|
|
f"**📢 [UPCOMING FREE] {game.title}**",
|
|
"> Coming soon — Free on Epic Games Store",
|
|
]
|
|
|
|
if game.end_date:
|
|
try:
|
|
end_dt = datetime.fromisoformat(game.end_date.replace("Z", "+00:00"))
|
|
date_str = end_dt.strftime("%B %-d")
|
|
lines.append(f"> 📅 _Free until {date_str}_")
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
if game.url:
|
|
lines.append(f"> 🔗 [Store Page]({game.url})")
|
|
|
|
md_text = "\n".join(lines)
|
|
html = _render_html(md_text)
|
|
|
|
plain_lines = [
|
|
f"📢 [UPCOMING FREE] {game.title}",
|
|
" Coming soon — Free on Epic Games Store",
|
|
]
|
|
if game.url:
|
|
plain_lines.append(f" 🔗 {game.url}")
|
|
|
|
plain_text = "\n".join(plain_lines)
|
|
|
|
return plain_text, html
|