13 Commits

Author SHA1 Message Date
Claude
0302cea523 Post Epic free games on first run instead of silently recording them
On first startup, _populate_initial_state() was marking current Epic free
games as "posted" without actually sending them to the room. This meant
users had to wait until the next cycle of new games before seeing anything.

Now only CheapShark deals are silently recorded on first run (there can be
dozens). Epic free games are left for check_epic_free_games() to pick up
and post immediately, since they are few in number and time-limited.

https://claude.ai/code/session_017eMsVwUopgmnEyd6JJedpV
2026-02-28 03:32:25 +00:00
Claude
c486b1f5c6 Add SEND_INTRO_MESSAGE to .env.example
https://claude.ai/code/session_017eMsVwUopgmnEyd6JJedpV
2026-02-28 03:07:29 +00:00
Claude
13cdd6d8b5 Document SEND_INTRO_MESSAGE in README config table
https://claude.ai/code/session_017eMsVwUopgmnEyd6JJedpV
2026-02-28 03:02:56 +00:00
Claude
703f92d46c Add .gitignore for Python bytecode files
https://claude.ai/code/session_017eMsVwUopgmnEyd6JJedpV
2026-02-28 03:01:49 +00:00
Claude
0fd58c32b4 Add optional intro message on bot startup
When SEND_INTRO_MESSAGE=true, the bot sends "The deals must flow." as
an m.notice to the Matrix room right after startup, before the first
scheduled deal checks. Defaults to off.

https://claude.ai/code/session_017eMsVwUopgmnEyd6JJedpV
2026-02-28 03:01:20 +00:00
Claude
3f7e8196e3 Sort CheapShark deals by recency instead of deal rating
The bot polls periodically for new deals, but sorting by "Deal Rating"
meant it always returned the same top-rated deals and would never
discover new listings that aren't in the top 10 by rating. Sorting by
"recent" ensures newly added or updated deals are seen on each poll.

https://claude.ai/code/session_017eMsVwUopgmnEyd6JJedpV
2026-02-28 02:49:28 +00:00
Claude
c33e46c3cf Fix CheapShark rating filter: correct scale and skip unrated deals
Two bugs fixed:
1. Function signature used old 0-100 scale defaults (min_rating: int = 80)
   but dealRating from CheapShark is 0-10. Updated to float = 8.0.
2. Deals with dealRating of 0.0 (unrated/insufficient data) were being
   filtered out as "poorly rated". Now treats 0.0 as unrated and skips
   the rating check, so games like Leisure Suit Larry that lack a
   CheapShark deal rating aren't incorrectly excluded.

https://claude.ai/code/session_017eMsVwUopgmnEyd6JJedpV
2026-02-28 02:47:15 +00:00
prosolis
c145d83b0b Merge pull request #6 from prosolis/claude/gaming-deals-matrix-bot-ONjS7
Fixed confusing rating for CheapShark, added debugging.
2026-02-27 18:34:08 -08:00
prosolis
7b05d86dc5 Merge pull request #5 from prosolis/claude/gaming-deals-matrix-bot-ONjS7
Load .env file at startup via python-dotenv
2026-02-27 18:05:40 -08:00
prosolis
b6b674ca0e Merge pull request #4 from prosolis/claude/gaming-deals-matrix-bot-ONjS7
Add --check preflight to validate config and connectivity
2026-02-27 17:54:31 -08:00
prosolis
c09ecdbd21 Merge pull request #3 from prosolis/claude/gaming-deals-matrix-bot-ONjS7
Add instructions for obtaining a Matrix bot access token
2026-02-27 17:45:15 -08:00
prosolis
aed260f345 Merge pull request #2 from prosolis/claude/gaming-deals-matrix-bot-ONjS7
Display all the main currencies
2026-02-27 16:58:54 -08:00
prosolis
ba1a3811d6 Merge pull request #1 from prosolis/claude/gaming-deals-matrix-bot-ONjS7
Add gaming deals Matrix bot
2026-02-27 16:07:45 -08:00
8 changed files with 51 additions and 12 deletions

View File

@@ -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
View File

@@ -0,0 +1,2 @@
__pycache__/
*.pyc

View File

@@ -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

View File

@@ -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()

View File

@@ -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."""

View File

@@ -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

View File

@@ -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")

View File

@@ -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()