Compare commits
13 Commits
claude/gam
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0302cea523 | ||
|
|
c486b1f5c6 | ||
|
|
13cdd6d8b5 | ||
|
|
703f92d46c | ||
|
|
0fd58c32b4 | ||
|
|
3f7e8196e3 | ||
|
|
c33e46c3cf | ||
|
|
c145d83b0b | ||
|
|
7b05d86dc5 | ||
|
|
b6b674ca0e | ||
|
|
c09ecdbd21 | ||
|
|
aed260f345 | ||
|
|
ba1a3811d6 |
@@ -12,5 +12,8 @@ MIN_DEAL_RATING=8.0
|
||||
MIN_DISCOUNT_PERCENT=50
|
||||
MAX_PRICE_USD=20
|
||||
|
||||
# Send an intro message to the room on startup (true/false)
|
||||
SEND_INTRO_MESSAGE=false
|
||||
|
||||
# Database path (inside the container, mount a volume for persistence)
|
||||
DATABASE_PATH=/data/deals.db
|
||||
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -63,6 +63,7 @@ All configuration is via environment variables (see `.env.example`):
|
||||
| `MIN_DEAL_RATING` | No | 8.0 | Minimum CheapShark deal rating (0-10) |
|
||||
| `MIN_DISCOUNT_PERCENT` | No | 50 | Minimum discount percentage |
|
||||
| `MAX_PRICE_USD` | No | 20 | Maximum sale price in USD |
|
||||
| `SEND_INTRO_MESSAGE` | No | false | Send "The deals must flow." to the room on startup |
|
||||
| `DATABASE_PATH` | No | deals.db | Path to SQLite database file |
|
||||
|
||||
## Preflight Check
|
||||
|
||||
@@ -69,6 +69,8 @@ async def main():
|
||||
scheduler.start()
|
||||
logger.info("Bot started — scheduler running")
|
||||
|
||||
await bot.send_intro()
|
||||
|
||||
# Run initial checks immediately (after first-run population is done)
|
||||
await bot.check_cheapshark()
|
||||
await bot.check_epic_free_games()
|
||||
|
||||
@@ -53,7 +53,13 @@ class DealsBot:
|
||||
await self.db.close()
|
||||
|
||||
async def _populate_initial_state(self):
|
||||
"""Fetch current deals and record them without posting (avoids spam on first run)."""
|
||||
"""Fetch current CheapShark deals and record them without posting (avoids spam on first run).
|
||||
|
||||
Epic free games are intentionally *not* recorded here so that the
|
||||
subsequent ``check_epic_free_games`` call will post them. There are
|
||||
only a handful at any time and they are time-limited, so users should
|
||||
see them immediately rather than waiting for the next cycle.
|
||||
"""
|
||||
deals = await fetch_deals(
|
||||
self._http,
|
||||
max_price=self.config.max_price_usd,
|
||||
@@ -63,14 +69,13 @@ class DealsBot:
|
||||
for deal in deals:
|
||||
await self.db.mark_posted(deal.dedup_id, "cheapshark", deal.title)
|
||||
|
||||
current_free, upcoming = await fetch_free_games(self._http)
|
||||
for game in current_free:
|
||||
await self.db.mark_posted(game.dedup_id, "epic", game.title)
|
||||
for game in upcoming:
|
||||
await self.db.mark_posted(game.dedup_id, "epic", game.title)
|
||||
logger.info("First run: recorded %d existing CheapShark deals", len(deals))
|
||||
|
||||
total = len(deals) + len(current_free) + len(upcoming)
|
||||
logger.info("First run: recorded %d existing deals/games", total)
|
||||
async def send_intro(self):
|
||||
"""Send an intro message to the Matrix room if configured."""
|
||||
if not self.config.send_intro_message:
|
||||
return
|
||||
await self.matrix.send_notice("The deals must flow.")
|
||||
|
||||
async def check_cheapshark(self):
|
||||
"""Poll CheapShark for deals and post new ones."""
|
||||
|
||||
@@ -46,8 +46,8 @@ async def fetch_deals(
|
||||
client: httpx.AsyncClient,
|
||||
*,
|
||||
max_price: float = 20,
|
||||
min_rating: int = 80,
|
||||
min_discount: int = 50,
|
||||
min_rating: float = 8.0,
|
||||
min_discount: float = 50,
|
||||
page_size: int = 10,
|
||||
) -> list[CheapSharkDeal]:
|
||||
"""Fetch top deals from CheapShark across configured stores."""
|
||||
@@ -55,7 +55,7 @@ async def fetch_deals(
|
||||
params = {
|
||||
"storeID": store_ids,
|
||||
"upperPrice": str(int(max_price)),
|
||||
"sortBy": "Deal Rating",
|
||||
"sortBy": "recent",
|
||||
"desc": "1",
|
||||
"pageSize": str(page_size),
|
||||
}
|
||||
@@ -81,7 +81,7 @@ async def fetch_deals(
|
||||
if savings < min_discount:
|
||||
logger.debug("Filtered out %s: savings %.1f%% < %.1f%%", title, savings, min_discount)
|
||||
continue
|
||||
if rating < min_rating:
|
||||
if rating > 0 and rating < min_rating:
|
||||
logger.debug("Filtered out %s: rating %.1f < %.1f", title, rating, min_rating)
|
||||
continue
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ class Config:
|
||||
self.min_discount_percent = int(os.environ.get("MIN_DISCOUNT_PERCENT", "50"))
|
||||
self.max_price_usd = float(os.environ.get("MAX_PRICE_USD", "20"))
|
||||
|
||||
# Intro message on startup
|
||||
self.send_intro_message = os.environ.get(
|
||||
"SEND_INTRO_MESSAGE", "false"
|
||||
).lower() in ("true", "1", "yes")
|
||||
|
||||
# Database
|
||||
self.database_path = os.environ.get("DATABASE_PATH", "deals.db")
|
||||
|
||||
|
||||
@@ -47,5 +47,26 @@ class MatrixDealsClient:
|
||||
logger.error("Matrix send error: %s", exc)
|
||||
return False
|
||||
|
||||
async def send_notice(self, text: str) -> bool:
|
||||
"""Send a plain m.notice (non-highlight) message to the configured room."""
|
||||
content = {
|
||||
"msgtype": "m.notice",
|
||||
"body": text,
|
||||
}
|
||||
|
||||
try:
|
||||
resp = await self._client.room_send(
|
||||
room_id=self.room_id,
|
||||
message_type="m.room.message",
|
||||
content=content,
|
||||
)
|
||||
if isinstance(resp, RoomSendError):
|
||||
logger.error("Failed to send notice: %s", resp.message)
|
||||
return False
|
||||
return True
|
||||
except Exception as exc:
|
||||
logger.error("Matrix send error: %s", exc)
|
||||
return False
|
||||
|
||||
async def close(self):
|
||||
await self._client.close()
|
||||
|
||||
Reference in New Issue
Block a user