Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .cursor/rules/rust-rule.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
alwaysApply: true
---

# Rust Expert Developer Guidelines

You are an expert Rust developer, highly skilled in async programming, concurrent systems, and modern Rust patterns. Your goal is to produce code that is professional, maintainable, readable, idiomatic, and performant.

## 1. Core Principles

- **Idiomatic Rust**: Adhere strictly to Rust idioms. Use `snake_case` for variables/functions and `PascalCase` for types.
- **Safety First**: Leverage the type system to enforce correctness. Avoid `unsafe` unless absolutely necessary.
- **Explicit Safety**: Every `unsafe` block _must_ be accompanied by a `// SAFETY:` comment explaining why the operation is sound.
- **Expressive & Clear**: Use descriptive variable names (`is_ready`, `has_data`) and avoid obscure abbreviations.
- **Feature-Driven Modularity**: Organize code by **feature**, not by file type. Keep related structs, enums, and `impl` blocks together in the same module.
- _Bad_: `types.rs`, `impls.rs`
- _Good_: `user.rs` containing `struct User` and `impl User`.

## 2. Code Structure & Patterns
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not be checked in


### 2.1 Structs & Types

- **Small & Cohesive**: Break complex data into smaller, composable structs.
- **Newtype Pattern**: Use tuple structs (e.g., `pub struct UserId(u64);`) to enforce type safety and prevent argument swapping.
- **Builder Pattern**: Use for complex initialization logic or structs with many optional fields. Avoid for simple constructors.
- **Generic Bounds**: Place generic bounds on `impl` blocks or functions, not on the struct definition, unless intrinsic to the type.

### 2.2 Async Programming (Tokio)

- **Runtime**: Use `tokio` as the default async runtime.
- **Structured Concurrency**: Use `tokio::spawn` for tasks and `tokio::select!` for managing multiple futures.
- **Synchronization**:
- Use `tokio::sync::mpsc` for asynchronous message passing.
- Use `tokio::sync::broadcast` for one-to-many communication.
- Use `tokio::sync::oneshot` for single responses.
- Prefer **bounded channels** to manage backpressure.
- Use `tokio::sync::Mutex` / `RwLock` for shared state in async contexts.
- **Performance**: Avoid blocking operations in async contexts. Offload CPU-bound work to `tokio::task::spawn_blocking`.

### 2.3 Error Handling

- **Result & Option**: Use `Result<T, E>` for recoverable errors. Use `?` for propagation.
- **Crates**: Use `thiserror` for library errors and `anyhow` for application-level error handling.
- **Panic**: Reserve `panic!` for unrecoverable bugs (logic errors).

## 3. Performance & Optimization

- **Collections**: Default to `Vec` and `HashMap`.
- **Pre-allocation**: Always use `Vec::with_capacity` or `HashMap::with_capacity` when the approximate size is known.
- **Cloning**: Be mindful of `.clone()`. Use references `&T` where ownership transfer isn't needed.

## 4. Testing

- **Unit Tests**: Place isolated tests in a `tests` module within the same file using `#[cfg(test)]`.
- **Async Tests**: Use `#[tokio::test]`.
- **Documentation Tests**: Use `rustdoc` examples in comments (`///`) to ensure documentation matches code behavior.
- **Mocking**: Use traits to define interfaces, allowing for easier mocking in tests.

## 5. Ecosystem & Config

- **Configuration**: Use environment variables (e.g., `dotenv`) for config management.
- **Crates**: Prefer standard ecosystem crates: `serde` (serialization), `reqwest` (HTTP), `sqlx` (DB), `tracing` (logging).

When asked to write code, always apply these rules to ensure the highest quality output.
6 changes: 6 additions & 0 deletions config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ client_secret = "lfXc45dZLqYTzP62Ms32EhXinGQzxcIP9TvjJml2B-h0T1nIJK"
api_key = "some-key"
interval_in_hours = 24
keywords = "quantum"
monthly_limit = 15000
alert_threshold = 13500
reset_day = 22

[tg_bot]
base_url = "https://api.telegram.org"
Expand All @@ -69,3 +72,6 @@ token = "token"
[raid_leaderboard]
sync_interval_in_hours = 24
tweets_req_interval_in_secs = 60

[alert]
webhook_url = "https://www.webhook_url.com"
6 changes: 6 additions & 0 deletions config/example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ client_secret = "example-secret"
api_key = "some-key"
interval_in_hours = 24
keywords = "example"
monthly_limit = 15000
alert_threshold = 13500
reset_day = 22

[tg_bot]
base_url = "https://api.telegram.org"
Expand All @@ -80,6 +83,9 @@ token = "token"
sync_interval_in_hours = 24
tweets_req_interval_in_secs = 60

[alert]
webhook_url = "https://www.webhook_url.com"

# Example environment variable overrides:
# TASKMASTER_BLOCKCHAIN__NODE_URL="ws://remote-node:9944"
# TASKMASTER_BLOCKCHAIN__WALLET_PASSWORD="super_secure_password"
Expand Down
6 changes: 6 additions & 0 deletions config/test.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ client_secret = "test-secret"
api_key = "some-key"
interval_in_hours = 24
keywords = "test"
monthly_limit = 15000
alert_threshold = 13500
reset_day = 22

[tg_bot]
base_url = "https://api.telegram.org"
Expand All @@ -69,3 +72,6 @@ token = "token"
[raid_leaderboard]
sync_interval_in_hours = 24
tweets_req_interval_in_secs = 1

[alert]
webhook_url = "https://www.webhook_url.com"
14 changes: 14 additions & 0 deletions migrations/009_tweet_pull_usage_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Table to track Twitter API usage for the monthly cap
CREATE TABLE IF NOT EXISTS tweet_pull_usage (
period VARCHAR(7) PRIMARY KEY, -- Format: YYYY-MM
tweet_count INTEGER DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Trigger for updated_at
DROP TRIGGER IF EXISTS set_timestamp ON tweet_pull_usage;
CREATE TRIGGER set_timestamp
BEFORE UPDATE ON tweet_pull_usage
FOR EACH ROW
EXECUTE PROCEDURE trigger_set_timestamp();

43 changes: 43 additions & 0 deletions scripts/seed_test_tweet_authors.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/bin/bash
set -e

# CONFIG ------------------------------------------
CONTAINER_NAME="task_master_test_db" # Change if your container is named differently
DB_USER="postgres"
DB_NAME="task_master"
SQL_FILE="seed_authors.sql"
# --------------------------------------------------

echo "🔧 Generating seed SQL..."

cat << 'EOF' > $SQL_FILE
INSERT INTO tweet_authors (
id, name, username, followers_count, following_count,
tweet_count, listed_count, like_count, media_count, fetched_at
)
VALUES
('1862779229277954048', 'Yuvi Lightman', 'YuviLightman', 0, 0, 0, 0, 0, 0, NOW())
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
username = EXCLUDED.username,
followers_count = EXCLUDED.followers_count,
following_count = EXCLUDED.following_count,
tweet_count = EXCLUDED.tweet_count,
listed_count = EXCLUDED.listed_count,
like_count = EXCLUDED.like_count,
media_count = EXCLUDED.media_count,
fetched_at = NOW();
EOF

echo "📦 Copying SQL file into container ($CONTAINER_NAME)..."
podman cp "$SQL_FILE" "$CONTAINER_NAME":/"$SQL_FILE"

echo "🚀 Running seed script inside Postgres..."
podman exec -it "$CONTAINER_NAME" psql -U "$DB_USER" -d "$DB_NAME" -f "/$SQL_FILE"

echo "🔍 Verifying result..."
podman exec -it "$CONTAINER_NAME" psql -U "$DB_USER" -d "$DB_NAME" -c "SELECT * FROM tweet_authors WHERE id = '1862779229277954048';"

rm -rf "$SQL_FILE"

echo "✅ Seeding complete!"
15 changes: 15 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub struct Config {
pub tweet_sync: TweetSyncConfig,
pub tg_bot: TelegramBotConfig,
pub raid_leaderboard: RaidLeaderboardConfig,
pub alert: AlertConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -74,6 +75,9 @@ pub struct TweetSyncConfig {
pub interval_in_hours: u64,
pub keywords: String,
pub api_key: String,
pub monthly_limit: u32,
pub alert_threshold: u32,
pub reset_day: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand All @@ -90,6 +94,11 @@ pub struct RaidLeaderboardConfig {
pub tweets_req_interval_in_secs: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertConfig {
pub webhook_url: String,
}

impl Config {
pub fn load(config_path: &str) -> Result<Self, config::ConfigError> {
let settings = config::Config::builder()
Expand Down Expand Up @@ -209,6 +218,9 @@ impl Default for Config {
interval_in_hours: 24,
keywords: "hello".to_string(),
api_key: "key".to_string(),
monthly_limit: 15000,
alert_threshold: 13000,
reset_day: 1,
},
tg_bot: TelegramBotConfig {
base_url: "https://api.telegram.org".to_string(),
Expand All @@ -220,6 +232,9 @@ impl Default for Config {
sync_interval_in_hours: 24,
tweets_req_interval_in_secs: 60,
},
alert: AlertConfig {
webhook_url: "https://your-webhook-url.com".to_string(),
},
}
}
}
6 changes: 6 additions & 0 deletions src/db_persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::repositories::raid_quest::RaidQuestRepository;
use crate::repositories::raid_submission::RaidSubmissionRepository;
use crate::repositories::relevant_tweet::RelevantTweetRepository;
use crate::repositories::tweet_author::TweetAuthorRepository;
use crate::repositories::tweet_pull_usage::TweetPullUsageRepository;
use crate::repositories::x_association::XAssociationRepository;
use crate::repositories::DbResult;
use crate::repositories::{
Expand Down Expand Up @@ -45,6 +46,7 @@ pub struct DbPersistence {
pub raid_quests: RaidQuestRepository,
pub raid_submissions: RaidSubmissionRepository,
pub raid_leaderboards: RaidLeaderboardRepository,
pub tweet_pull_usage: TweetPullUsageRepository,

pub pool: PgPool,
}
Expand All @@ -67,6 +69,7 @@ impl DbPersistence {
let raid_quests = RaidQuestRepository::new(&pool);
let raid_submissions = RaidSubmissionRepository::new(&pool);
let raid_leaderboards = RaidLeaderboardRepository::new(&pool);
let tweet_pull_usage = TweetPullUsageRepository::new(pool.clone());

Ok(Self {
pool,
Expand All @@ -82,6 +85,7 @@ impl DbPersistence {
raid_quests,
raid_submissions,
raid_leaderboards,
tweet_pull_usage,
})
}

Expand All @@ -101,6 +105,7 @@ impl DbPersistence {
let raid_quests = RaidQuestRepository::new(&pool);
let raid_submissions = RaidSubmissionRepository::new(&pool);
let raid_leaderboards = RaidLeaderboardRepository::new(&pool);
let tweet_pull_usage = TweetPullUsageRepository::new(pool.clone());

Ok(Self {
pool,
Expand All @@ -116,6 +121,7 @@ impl DbPersistence {
raid_quests,
raid_submissions,
raid_leaderboards,
tweet_pull_usage,
})
}
}
4 changes: 4 additions & 0 deletions src/http_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::{
metrics::{metrics_handler, track_metrics, Metrics},
models::task::TaskStatus,
routes::api_routes,
services::alert_service::AlertService,
Config, GraphqlClient,
};
use chrono::{DateTime, Utc};
Expand All @@ -29,6 +30,7 @@ pub struct AppState {
pub oauth_sessions: Arc<Mutex<HashMap<String, PkceCodeVerifier>>>,
pub twitter_oauth_tokens: Arc<RwLock<HashMap<String, String>>>,
pub twitter_gateway: Arc<dyn TwitterGateway>,
pub alert_client: Arc<AlertService>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -115,13 +117,15 @@ pub async fn start_server(
db: Arc<DbPersistence>,
graphql_client: Arc<GraphqlClient>,
twitter_gateway: Arc<dyn TwitterGateway>,
alert_client: Arc<AlertService>,
bind_address: &str,
config: Arc<Config>,
) -> Result<(), Box<dyn std::error::Error>> {
let state = AppState {
db,
metrics: Arc::new(Metrics::new()),
graphql_client,
alert_client: alert_client,
config,
twitter_gateway,
challenges: Arc::new(RwLock::new(HashMap::new())),
Expand Down
9 changes: 7 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
errors::{AppError, AppResult},
models::task::{Task, TaskInput},
services::{
graphql_client::GraphqlClient, raid_leaderboard_service::RaidLeaderboardService,
alert_service::AlertService, graphql_client::GraphqlClient, raid_leaderboard_service::RaidLeaderboardService,
reverser::start_reverser_service, task_generator::TaskGenerator, telegram_service::TelegramService,
transaction_manager::TransactionManager, tweet_synchronizer_service::TweetSynchronizerService,
},
Expand Down Expand Up @@ -271,16 +271,19 @@ async fn main() -> AppResult<()> {
Some(config.tweet_sync.api_key.clone()),
)?);
let telegram_service = Arc::new(TelegramService::new(config.tg_bot.clone()));
let alert_service = Arc::new(AlertService::new(config.clone(), db.tweet_pull_usage.clone()));
let server_db = db.clone();
let graphql_client = Arc::new(graphql_client.clone());
let server_addr_clone = server_address.clone();
let server_config = Arc::new(config.clone());
let server_twitter_gateway = twitter_gateway.clone();
let server_alert_service = alert_service.clone();
let server_task = tokio::spawn(async move {
http_server::start_server(
server_db,
graphql_client,
server_twitter_gateway,
server_alert_service,
&server_addr_clone,
server_config,
)
Expand All @@ -305,11 +308,13 @@ async fn main() -> AppResult<()> {
db.clone(),
twitter_gateway.clone(),
telegram_service,
alert_service.clone(),
Arc::new(config.clone()),
);

// Initialize raid leaderboard service
let raid_leaderboard_service = RaidLeaderboardService::new(db.clone(), twitter_gateway, Arc::new(config.clone()));
let raid_leaderboard_service =
RaidLeaderboardService::new(db.clone(), twitter_gateway, alert_service, Arc::new(config.clone()));

// Wait for any task to complete (they should run forever unless there's an error)
tokio::select! {
Expand Down
1 change: 1 addition & 0 deletions src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ pub mod referrals;
pub mod relevant_tweet;
pub mod task;
pub mod tweet_author;
pub mod tweet_pull_usage;
pub mod x_association;
11 changes: 11 additions & 0 deletions src/models/tweet_pull_usage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use chrono::{DateTime, Utc};

#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct TweetPullUsage {
pub period: String,
pub tweet_count: i32,
pub updated_at: DateTime<Utc>,
}

1 change: 1 addition & 0 deletions src/repositories/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod referral;
pub mod relevant_tweet;
pub mod task;
pub mod tweet_author;
pub mod tweet_pull_usage;
pub mod x_association;

pub trait QueryBuilderExt {
Expand Down
Loading