Skip to content

Commit 4f9e415

Browse files
authored
Python rewrite with charts and slash commands (#16)
* fix: add timeout to health check endpoints to prevent hanging - Added 5 second timeout to /health and /health/all endpoints - If health check takes longer than 5s, returns unhealthy instead of hanging - Refactored handlers to use timeout utility from tokio::time - Fixed return type issue where Err variant had wrong format * fix: use tokio::sync::Mutex instead of std::sync::Mutex in HealthAggregator The std::sync::Mutex was causing the async tokio runtime to block when the health check tried to acquire the lock, leading to health check timeouts and Discord gateway disconnects. Changed to tokio::sync::Mutex which properly yields to the scheduler when contended. * fix: add Discord connectivity monitoring and auto-restart logic - Fix periodic Discord connectivity test: was mathematically impossible (mod 4 vs mod 10) - Fix loop exit logic: return Err in async block didn't exit loop, use break instead - Fix fire-and-forget periodic test: now awaits and checks failures synchronously - Fix Mutex API usage: tokio::sync::Mutex returns guard directly, not Result - Add time-based connectivity check every 60 seconds - Add restart triggers: gateway_failures>=5, discord_test_failures>=3, no Discord success >2min - Remove unreachable!() panic in format_custom_status * fix: remove unreachable!() panics in format_custom_status Change % 4 to % 40 for update_count to allow periodic connectivity test to trigger, but update all match expressions to use update_count % 4 so they cycle properly instead of panicking on values 4-39 * fix: use subquery for DELETE LIMIT to support older SQLite versions SQLite doesn't support DELETE ... LIMIT until version 3.35.0. Wrap LIMIT in subquery to delete by rowid instead. * fix: improve SQLite concurrency settings for better handling of many bots - Increased busy_timeout to 60s - Increased pool size from 4 to 16 connections - Added min_idle of 4 connections - Added 64MB cache - Added temp_store = MEMORY Note: PostgreSQL migration attempted but requires async rewrite of all call sites * feat: migrate from SQLite to PostgreSQL for better concurrency - Replace rusqlite/r2d2 with async sqlx and PgPool - All database methods are now async fn - Add PostgreSQL service to docker-compose.yml with healthcheck - DATABASE_URL env var replaces DATABASE_PATH - Update all code to use .await for database calls - PostgreSQL eliminates 'database is locked' errors with 24+ bots * feat: improve stability with task supervision and shared price state Issue 4 - DB Write Failures: - Add db_failures counter to HealthState - Increment on DB write failure, reset on success - Mark unhealthy if db_failures > 3 Issue 5 - Health Server Bind Failure: - Add start_health_server_with_retry() with 10 retries - Health server bind failure is now non-fatal - Bots continue running even if health server fails Issues 1 & 6 - Silent Task Death and Supervision: - All spawned tasks now have supervision loops - Services auto-restart on unexpected exit - Store and monitor all service join handles - Log CRITICAL errors when services die unexpectedly Issue 2 - Race Condition on prices.json: - Add SharedPrices with tokio::sync::RwLock - Price service writes to shared state atomically first - Bots read from shared state instead of file - File write happens AFTER atomic state update * fix: compilation errors in stability migration - Fix syntax error in database.rs (extra parenthesis) - Fix Arc/SharedPrices type mismatches across modules - Fix i64 vs u64 type casts in db_cleanup.rs - Fix duplicate Arc import in price_service.rs - Fix private struct re-exports from price_state - Fix ticker move before use in async spawn loop - Fix health_server return type in match arm * feat(v2): Python rewrite with discord.py and asyncpg - Python bot using discord.py (more stable Discord ecosystem) - PostgreSQL via asyncpg (native async, connection pooling) - Pyth Network API for price feeds - Simple docker-compose with build inside - Environment-based configuration for bot tokens * fix: asyncio.run() and idempotent table creation - Use asyncio.run() to properly run async tasks - Catch table/index creation errors gracefully - Allow multiple bots to initialize DB concurrently * feat: cycle through BTC/ETH/SOL with 1h percentage change - Status cycles through BTC, ETH, SOL every 30 seconds - Shows 1h percentage change from database - Nickname shows crypto and formatted price * fix: each bot shows its own crypto price, not cycling * fix: SHANGHAISILVER fallback to cached price if fetch fails - Shanghai Silver website is JS-rendered, scrape doesn't work - Only update SHANGHAISILVER if price > 10 (silver is ~30+) - Otherwise use cached database price * fix: SHANGHAISVER - goldsilver.ai is fully JS-rendered, no live data available - Site uses client-side JavaScript rendering, cannot scrape - Rust version may have worked with different site configuration - SHANGHAISILVER now shows no price until alternative source found - Other cryptos (BTC, ETH, SOL, etc.) work fine with Pyth Network * feat: SHANGHAISILVER price fetch works! - Fixed extraction to find dollar amounts after 'Shanghai Spot' - Pattern: find prefix, then look for $ followed by number - Shanghai Silver now fetches ~$92 from goldsilver.ai * fix: uppercase ticker names in Discord nicknames * feat: DXY via Yahoo Finance (DX-Y.NYB) - Added Yahoo Finance API support for DXY - Uses chart API endpoint with regularMarketPrice - DXY now shows ~98 in Discord * feat: cycle status between BTC value and 1h percentage - Nickname: CRYPTO - Status cycles: BTC value (e.g., ₿0.024421) then 1h change, then BTC, etc. - For BTC bot, only shows 1h percentage (no self-conversion) * fix: show BTC value as '0.030582 BTC' instead of ₿ symbol * feat: cycle through BTC, ETH, SOL conversions and 1h% - Status cycles: BTC value, ETH value, SOL value, 1h%, then repeat - Each crypto shows its value in terms of the other three - For BTC, shows ETH, SOL, 1h% (no self-conversion) - For ETH, shows BTC, SOL, 1h% (no self-conversion) - Same for SOL * docs: update README for Python rewrite - Clean documentation without emojis or special characters - Updated architecture diagram for PostgreSQL - Removed Rust/SQLite/Plotters references - Added DXY, SHANGHAISILVER, GOLD/SILVER sources - Document status cycling behavior - Added project structure section * Add chart generation commands and improve price display - Added chart_service.py with matplotlib for beautiful dark-themed charts - Added /chart price command showing 24h/7d/30d price charts with high/low markers - Added /price current command with USD price, 24h/7d/30d percentage changes, and BTC/ETH/SOL conversions - Changed SHANGHAISILVER ticker to SSILVER for brevity - Added font dependencies to Dockerfile for matplotlib - Updated README with slash commands documentation - Chart commands scoped per-bot (BTC bot shows BTC charts only, etc.) * Fix /price command to default to bot's own ticker * Enable /chart command for all bots * Add timeframe presets to /chart command * Update README with SSILVER display name * Expand /chart price documentation in README * Add autocomplete timeframe input to /chart command * Switch to Alpine for smaller image size (266MB vs 350MB) * Rewrite README with clean, professional documentation * Fix code review findings: chart dates, resource cleanup, remove dead code * Security fixes: non-root user, fail-fast DATABASE_URL, env-based postgres credentials * Enforce strong postgres password via env vars * Add CI/CD with ruff linting and docker smoke test * Fix CI: use docker compose v2 syntax, update action versions * Fix CI: lint cleanup, fallback postgres password for CI * Fix lint errors and CI env file setup * Update CI: checkout@v5, opt into Node.js 24 * Fix PR review: row limits, downsampling, save conversion prices * Implement 5-year retention with tiered aggregation - Raw data: last 24h - 5-min aggregates: 7 days - Hourly aggregates: 30 days - Daily aggregates: 1 year - Weekly aggregates: 5 years - Older data: auto-deleted Chart queries auto-select appropriate aggregation level. Maintenance runs hourly, cleanup runs daily. * Fix PR review: DOUBLE PRECISION for prices, SSILVER fallback logic * Fix lint errors in database.py
1 parent 7bda853 commit 4f9e415

31 files changed

Lines changed: 1315 additions & 9114 deletions

.cargo/config.toml

Lines changed: 0 additions & 14 deletions
This file was deleted.

.env.example

Lines changed: 12 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,17 @@
1-
# Price update interval (shared by all services)
2-
UPDATE_INTERVAL_SECONDS=30
1+
# Price update interval in seconds
2+
UPDATE_INTERVAL_SECONDS=12
33

4-
# How often to clean up old price data (in hours)
5-
CLEANUP_INTERVAL_HOURS=48
6-
7-
# Log level: error, warn, info, debug, trace
8-
RUST_LOG=info
9-
10-
# Discord Bot Tokens - get these from https://discord.com/developers/applications
11-
# Create a bot, go to Bot, and click "Reset Token" to get a new token
12-
# Then invite the bot to your server with appropriate permissions
4+
# Discord Bot Tokens
5+
# Format: DISCORD_TOKEN_<NAME>=token
136
DISCORD_TOKEN_BTC=your_btc_bot_token_here
147
DISCORD_TOKEN_ETH=your_eth_bot_token_here
158
DISCORD_TOKEN_SOL=your_sol_bot_token_here
16-
DISCORD_TOKEN_DOGE=your_doge_bot_token_here
17-
DISCORD_TOKEN_AVAX=your_avax_bot_token_here
18-
DISCORD_TOKEN_BNB=your_bnb_bot_token_here
19-
DISCORD_TOKEN_SUI=your_sui_bot_token_here
20-
DISCORD_TOKEN_SEI=your_sei_bot_token_here
21-
DISCORD_TOKEN_JLP=your_jlp_bot_token_here
22-
DISCORD_TOKEN_PUMP=your_pump_bot_token_here
23-
DISCORD_TOKEN_XPL=your_xpl_bot_token_here
24-
DISCORD_TOKEN_MSTR=your_mstr_bot_token_here
25-
DISCORD_TOKEN_OIL=your_oil_bot_token_here
26-
DISCORD_TOKEN_VOO=your_voo_bot_token_here
27-
DISCORD_TOKEN_DXY=your_dxy_bot_token_here
28-
DISCORD_TOKEN_HOOD=your_hood_bot_token_here
29-
DISCORD_TOKEN_SBET=your_sbet_bot_token_here
30-
DISCORD_TOKEN_GOLD=your_gold_bot_token_here
31-
DISCORD_TOKEN_SILVER=your_silver_bot_token_here
32-
DISCORD_TOKEN_FARTCOIN=your_fartcoin_bot_token_here
33-
DISCORD_TOKEN_2Z=your_2z_bot_token_here
34-
DISCORD_TOKEN_ASTER=your_aster_bot_token_here
35-
DISCORD_TOKEN_EURO=your_euro_bot_token_here
36-
DISCORD_TOKEN_SHANGHAISILVER=your_shanghai_silver_bot_token_here
37-
DISCORD_TOKEN_SHANGHAI=your_shanghai_bot_token_here
389

39-
# Crypto Feed IDs (Pyth Network) - these are public and safe to share
40-
# Get fresh IDs from https://pyth.network/docs/developers
41-
CRYPTO_FEEDS=BTC:0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43,ETH:0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace,SOL:0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d,DOGE:0xdcef50dd0a4cd2dcc17e45df1676dcb336a11a61c69df7a0299b0150c672d25c,DXY:yahoo_finance
10+
# Pyth Network Feed IDs (comma-separated)
11+
# Format: CRYPTO:feed_id,CRYPTO:feed_id,...
12+
CRYPTO_FEEDS=BTC:0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43,ETH:0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace,SOL:0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d
13+
14+
# Database - CHANGE THESE PASSWORDS
15+
POSTGRES_USER=postgres
16+
POSTGRES_PASSWORD=PdefSMMIa8N22nKwHxmWz5znC13bUFo
17+
DATABASE_URL=postgresql://postgres:PdefSMMIa8N22nKwHxmWz5znC13bUFo@postgres:5432/pricebot

.factory/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"enabledPlugins": {
3+
"core@factory-plugins": true
4+
}
5+
}

.github/workflows/ci.yml

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,83 @@ name: CI
22

33
on:
44
push:
5-
branches: [main, dev]
5+
branches: [main, v2]
66
pull_request:
7-
branches: [main]
7+
branches: [main, v2]
8+
9+
env:
10+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
811

912
jobs:
10-
test:
13+
lint:
14+
name: Lint Python
1115
runs-on: ubuntu-latest
1216
steps:
13-
- uses: actions/checkout@v4
14-
15-
- name: Install Rust
16-
uses: dtolnay/rust-toolchain@stable
17-
18-
- name: Install system dependencies
19-
run: |
20-
sudo apt-get update
21-
sudo apt-get install -y pkg-config libssl-dev libfreetype6-dev libfontconfig1-dev
17+
- uses: actions/checkout@v5
2218

23-
- name: Cache cargo
24-
uses: Swatinem/rust-cache@v2
19+
- name: Set up Python
20+
uses: actions/setup-python@v6
21+
with:
22+
python-version: "3.12"
2523

26-
- name: Build
27-
run: cargo build --release
24+
- name: Install lint tools
25+
run: pip install ruff
2826

29-
- name: Run tests
30-
run: cargo test
27+
- name: Run ruff
28+
run: ruff check .
3129

3230
docker:
31+
name: Docker Build
3332
runs-on: ubuntu-latest
3433
steps:
35-
- uses: actions/checkout@v4
34+
- uses: actions/checkout@v5
3635

37-
- name: Set up Docker Buildx
38-
uses: docker/setup-buildx-action@v3
36+
- name: Create test .env
37+
run: |
38+
cp .env.example .env
39+
echo "DATABASE_URL=postgresql://postgres:postgres@postgres:5432/pricebot" >> .env
3940
40-
- name: Build Docker image
41-
uses: docker/build-push-action@v5
42-
with:
43-
context: .
44-
load: true
45-
tags: rustymcpriceface:test
41+
- name: Build Docker Compose
42+
run: docker compose build
43+
44+
- name: Check Docker Compose validity
45+
run: docker compose config --quiet
46+
47+
smoke-test:
48+
name: Smoke Test
49+
runs-on: ubuntu-latest
50+
steps:
51+
- uses: actions/checkout@v5
4652

47-
- name: Verify Docker image runs
53+
- name: Create test .env
4854
run: |
49-
docker run -d --name test-container -e DISCORD_TOKEN_BTC=test rustymcpriceface:test
50-
sleep 10
51-
docker logs test-container
52-
docker rm -f test-container
55+
cp .env.example .env
56+
echo "DATABASE_URL=postgresql://postgres:postgres@postgres:5432/pricebot" >> .env
57+
58+
- name: Start services
59+
run: docker compose up -d
60+
61+
- name: Wait for postgres
62+
run: |
63+
for i in {1..30}; do
64+
if docker compose exec -T postgres pg_isready -U postgres > /dev/null 2>&1; then
65+
echo "Postgres ready"
66+
exit 0
67+
fi
68+
sleep 1
69+
done
70+
echo "Postgres failed to start"
71+
exit 1
72+
73+
- name: Check bot container
74+
run: |
75+
sleep 5
76+
docker compose ps
77+
78+
- name: Check bot logs
79+
run: |
80+
docker compose logs bot | head -20
81+
82+
- name: Stop services
83+
if: always()
84+
run: docker compose down

.github/workflows/test.yml

Lines changed: 0 additions & 30 deletions
This file was deleted.

0 commit comments

Comments
 (0)