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
|
MIN_DISCOUNT_PERCENT=50
|
||||||
MAX_PRICE_USD=20
|
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 (inside the container, mount a volume for persistence)
|
||||||
DATABASE_PATH=/data/deals.db
|
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_DEAL_RATING` | No | 8.0 | Minimum CheapShark deal rating (0-10) |
|
||||||
| `MIN_DISCOUNT_PERCENT` | No | 50 | Minimum discount percentage |
|
| `MIN_DISCOUNT_PERCENT` | No | 50 | Minimum discount percentage |
|
||||||
| `MAX_PRICE_USD` | No | 20 | Maximum sale price in USD |
|
| `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 |
|
| `DATABASE_PATH` | No | deals.db | Path to SQLite database file |
|
||||||
|
|
||||||
## Preflight Check
|
## Preflight Check
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ async def main():
|
|||||||
scheduler.start()
|
scheduler.start()
|
||||||
logger.info("Bot started — scheduler running")
|
logger.info("Bot started — scheduler running")
|
||||||
|
|
||||||
|
await bot.send_intro()
|
||||||
|
|
||||||
# Run initial checks immediately (after first-run population is done)
|
# Run initial checks immediately (after first-run population is done)
|
||||||
await bot.check_cheapshark()
|
await bot.check_cheapshark()
|
||||||
await bot.check_epic_free_games()
|
await bot.check_epic_free_games()
|
||||||
|
|||||||
@@ -53,7 +53,13 @@ class DealsBot:
|
|||||||
await self.db.close()
|
await self.db.close()
|
||||||
|
|
||||||
async def _populate_initial_state(self):
|
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(
|
deals = await fetch_deals(
|
||||||
self._http,
|
self._http,
|
||||||
max_price=self.config.max_price_usd,
|
max_price=self.config.max_price_usd,
|
||||||
@@ -63,14 +69,13 @@ class DealsBot:
|
|||||||
for deal in deals:
|
for deal in deals:
|
||||||
await self.db.mark_posted(deal.dedup_id, "cheapshark", deal.title)
|
await self.db.mark_posted(deal.dedup_id, "cheapshark", deal.title)
|
||||||
|
|
||||||
current_free, upcoming = await fetch_free_games(self._http)
|
logger.info("First run: recorded %d existing CheapShark deals", len(deals))
|
||||||
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)
|
|
||||||
|
|
||||||
total = len(deals) + len(current_free) + len(upcoming)
|
async def send_intro(self):
|
||||||
logger.info("First run: recorded %d existing deals/games", total)
|
"""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):
|
async def check_cheapshark(self):
|
||||||
"""Poll CheapShark for deals and post new ones."""
|
"""Poll CheapShark for deals and post new ones."""
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ async def fetch_deals(
|
|||||||
client: httpx.AsyncClient,
|
client: httpx.AsyncClient,
|
||||||
*,
|
*,
|
||||||
max_price: float = 20,
|
max_price: float = 20,
|
||||||
min_rating: int = 80,
|
min_rating: float = 8.0,
|
||||||
min_discount: int = 50,
|
min_discount: float = 50,
|
||||||
page_size: int = 10,
|
page_size: int = 10,
|
||||||
) -> list[CheapSharkDeal]:
|
) -> list[CheapSharkDeal]:
|
||||||
"""Fetch top deals from CheapShark across configured stores."""
|
"""Fetch top deals from CheapShark across configured stores."""
|
||||||
@@ -55,7 +55,7 @@ async def fetch_deals(
|
|||||||
params = {
|
params = {
|
||||||
"storeID": store_ids,
|
"storeID": store_ids,
|
||||||
"upperPrice": str(int(max_price)),
|
"upperPrice": str(int(max_price)),
|
||||||
"sortBy": "Deal Rating",
|
"sortBy": "recent",
|
||||||
"desc": "1",
|
"desc": "1",
|
||||||
"pageSize": str(page_size),
|
"pageSize": str(page_size),
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ async def fetch_deals(
|
|||||||
if savings < min_discount:
|
if savings < min_discount:
|
||||||
logger.debug("Filtered out %s: savings %.1f%% < %.1f%%", title, savings, min_discount)
|
logger.debug("Filtered out %s: savings %.1f%% < %.1f%%", title, savings, min_discount)
|
||||||
continue
|
continue
|
||||||
if rating < min_rating:
|
if rating > 0 and rating < min_rating:
|
||||||
logger.debug("Filtered out %s: rating %.1f < %.1f", title, rating, min_rating)
|
logger.debug("Filtered out %s: rating %.1f < %.1f", title, rating, min_rating)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ class Config:
|
|||||||
self.min_discount_percent = int(os.environ.get("MIN_DISCOUNT_PERCENT", "50"))
|
self.min_discount_percent = int(os.environ.get("MIN_DISCOUNT_PERCENT", "50"))
|
||||||
self.max_price_usd = float(os.environ.get("MAX_PRICE_USD", "20"))
|
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
|
# Database
|
||||||
self.database_path = os.environ.get("DATABASE_PATH", "deals.db")
|
self.database_path = os.environ.get("DATABASE_PATH", "deals.db")
|
||||||
|
|
||||||
|
|||||||
@@ -47,5 +47,26 @@ class MatrixDealsClient:
|
|||||||
logger.error("Matrix send error: %s", exc)
|
logger.error("Matrix send error: %s", exc)
|
||||||
return False
|
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):
|
async def close(self):
|
||||||
await self._client.close()
|
await self._client.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user