mirror of
https://github.com/Xevion/banner.git
synced 2025-12-05 23:14:20 -06:00
Compare commits
2 Commits
1b7d2d2824
...
8384f418c8
| Author | SHA1 | Date | |
|---|---|---|---|
| 8384f418c8 | |||
| 3dca896a35 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -218,7 +218,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "banner"
|
||||
version = "0.3.3"
|
||||
version = "0.3.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "banner"
|
||||
version = "0.3.3"
|
||||
version = "0.3.4"
|
||||
edition = "2024"
|
||||
default-run = "banner"
|
||||
|
||||
@@ -44,7 +44,7 @@ governor = "0.10.1"
|
||||
once_cell = "1.21.3"
|
||||
serde_path_to_error = "0.1.17"
|
||||
num-format = "0.4.4"
|
||||
tower-http = { version = "0.6.0", features = ["fs", "cors", "trace"] }
|
||||
tower-http = { version = "0.6.0", features = ["fs", "cors", "trace", "timeout"] }
|
||||
rust-embed = { version = "8.0", features = ["debug-embed", "include-exclude"] }
|
||||
mime_guess = "2.0"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::banner::{
|
||||
BannerSession, SessionPool, create_shared_rate_limiter_with_config,
|
||||
BannerSession, SessionPool, create_shared_rate_limiter,
|
||||
errors::BannerApiError,
|
||||
json::parse_json_with_context,
|
||||
middleware::TransparentMiddleware,
|
||||
@@ -15,7 +15,7 @@ use crate::banner::{
|
||||
nonce,
|
||||
query::SearchQuery,
|
||||
rate_limit_middleware::RateLimitMiddleware,
|
||||
rate_limiter::{RateLimitConfig, SharedRateLimiter, create_shared_rate_limiter},
|
||||
rate_limiter::{RateLimitConfig, SharedRateLimiter},
|
||||
util::user_agent,
|
||||
};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
@@ -35,6 +35,7 @@ pub struct BannerApi {
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl BannerApi {
|
||||
/// Creates a new Banner API client.
|
||||
pub fn new(base_url: String) -> Result<Self> {
|
||||
@@ -43,7 +44,7 @@ impl BannerApi {
|
||||
|
||||
/// Creates a new Banner API client with custom rate limiting configuration.
|
||||
pub fn new_with_config(base_url: String, rate_limit_config: RateLimitConfig) -> Result<Self> {
|
||||
let rate_limiter = create_shared_rate_limiter_with_config(rate_limit_config);
|
||||
let rate_limiter = create_shared_rate_limiter(Some(rate_limit_config));
|
||||
|
||||
let http = ClientBuilder::new(
|
||||
Client::builder()
|
||||
|
||||
@@ -258,6 +258,7 @@ impl TimeRange {
|
||||
}
|
||||
|
||||
/// Get duration in minutes
|
||||
#[allow(dead_code)]
|
||||
pub fn duration_minutes(&self) -> i64 {
|
||||
let start_minutes = self.start.hour() as i64 * 60 + self.start.minute() as i64;
|
||||
let end_minutes = self.end.hour() as i64 * 60 + self.end.minute() as i64;
|
||||
@@ -302,6 +303,7 @@ impl DateRange {
|
||||
}
|
||||
|
||||
/// Check if a specific date falls within this range
|
||||
#[allow(dead_code)]
|
||||
pub fn contains_date(&self, date: NaiveDate) -> bool {
|
||||
date >= self.start && date <= self.end
|
||||
}
|
||||
|
||||
@@ -147,11 +147,6 @@ impl Term {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a long string representation of the term (e.g., "Fall 2025")
|
||||
pub fn to_long_string(&self) -> String {
|
||||
format!("{} {}", self.season, self.year)
|
||||
}
|
||||
}
|
||||
|
||||
impl TermPoint {
|
||||
|
||||
@@ -32,6 +32,7 @@ pub struct SearchQuery {
|
||||
course_number_range: Option<Range>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl SearchQuery {
|
||||
/// Creates a new SearchQuery with default values
|
||||
pub fn new() -> Self {
|
||||
|
||||
@@ -61,7 +61,6 @@ pub struct BannerRateLimiter {
|
||||
search_limiter: RateLimiter<NotKeyed, InMemoryState, DefaultClock>,
|
||||
metadata_limiter: RateLimiter<NotKeyed, InMemoryState, DefaultClock>,
|
||||
reset_limiter: RateLimiter<NotKeyed, InMemoryState, DefaultClock>,
|
||||
config: RateLimitConfig,
|
||||
}
|
||||
|
||||
impl BannerRateLimiter {
|
||||
@@ -88,7 +87,6 @@ impl BannerRateLimiter {
|
||||
search_limiter: RateLimiter::direct(search_quota),
|
||||
metadata_limiter: RateLimiter::direct(metadata_quota),
|
||||
reset_limiter: RateLimiter::direct(reset_quota),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,32 +106,6 @@ impl BannerRateLimiter {
|
||||
|
||||
trace!(request_type = ?request_type, "Rate limit permission granted");
|
||||
}
|
||||
|
||||
/// Checks if a request of the given type would be allowed immediately
|
||||
pub fn check_permission(&self, request_type: RequestType) -> bool {
|
||||
let limiter = match request_type {
|
||||
RequestType::Session => &self.session_limiter,
|
||||
RequestType::Search => &self.search_limiter,
|
||||
RequestType::Metadata => &self.metadata_limiter,
|
||||
RequestType::Reset => &self.reset_limiter,
|
||||
};
|
||||
|
||||
limiter.check().is_ok()
|
||||
}
|
||||
|
||||
/// Gets the current configuration
|
||||
pub fn config(&self) -> &RateLimitConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Updates the rate limit configuration
|
||||
pub fn update_config(&mut self, config: RateLimitConfig) {
|
||||
self.config = config;
|
||||
// Note: In a production system, you'd want to recreate the limiters
|
||||
// with the new configuration, but for simplicity we'll just update
|
||||
// the config field here.
|
||||
warn!("Rate limit configuration updated - restart required for full effect");
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BannerRateLimiter {
|
||||
@@ -145,14 +117,9 @@ impl Default for BannerRateLimiter {
|
||||
/// A shared rate limiter instance
|
||||
pub type SharedRateLimiter = Arc<BannerRateLimiter>;
|
||||
|
||||
/// Creates a new shared rate limiter with default configuration
|
||||
pub fn create_shared_rate_limiter() -> SharedRateLimiter {
|
||||
Arc::new(BannerRateLimiter::default())
|
||||
}
|
||||
|
||||
/// Creates a new shared rate limiter with custom configuration
|
||||
pub fn create_shared_rate_limiter_with_config(config: RateLimitConfig) -> SharedRateLimiter {
|
||||
Arc::new(BannerRateLimiter::new(config))
|
||||
pub fn create_shared_rate_limiter(config: Option<RateLimitConfig>) -> SharedRateLimiter {
|
||||
Arc::new(BannerRateLimiter::new(config.unwrap_or_default()))
|
||||
}
|
||||
|
||||
/// Conversion from config module's RateLimitingConfig to this module's RateLimitConfig
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_json::Value;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(sqlx::FromRow, Debug, Clone)]
|
||||
pub struct Course {
|
||||
pub id: i32,
|
||||
@@ -18,6 +19,7 @@ pub struct Course {
|
||||
pub last_scraped_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(sqlx::FromRow, Debug, Clone)]
|
||||
pub struct CourseMetric {
|
||||
pub id: i32,
|
||||
@@ -28,6 +30,7 @@ pub struct CourseMetric {
|
||||
pub seats_available: i32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(sqlx::FromRow, Debug, Clone)]
|
||||
pub struct CourseAudit {
|
||||
pub id: i32,
|
||||
@@ -59,6 +62,7 @@ pub enum TargetType {
|
||||
}
|
||||
|
||||
/// Represents a queryable job from the database.
|
||||
#[allow(dead_code)]
|
||||
#[derive(sqlx::FromRow, Debug, Clone)]
|
||||
pub struct ScrapeJob {
|
||||
pub id: i32,
|
||||
|
||||
@@ -229,9 +229,7 @@ async fn main() {
|
||||
let app_state = AppState::new(banner_api_arc.clone(), db_pool.clone());
|
||||
|
||||
// Create BannerState for web service
|
||||
let banner_state = BannerState {
|
||||
api: banner_api_arc.clone(),
|
||||
};
|
||||
let banner_state = BannerState {};
|
||||
|
||||
// Configure the client with your Discord bot token in the environment
|
||||
let intents = GatewayIntents::non_privileged();
|
||||
|
||||
@@ -12,7 +12,6 @@ use std::fmt;
|
||||
pub enum JobParseError {
|
||||
InvalidJson(serde_json::Error),
|
||||
UnsupportedTargetType(TargetType),
|
||||
MissingRequiredField(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for JobParseError {
|
||||
@@ -22,9 +21,6 @@ impl fmt::Display for JobParseError {
|
||||
JobParseError::UnsupportedTargetType(t) => {
|
||||
write!(f, "Unsupported target type: {:?}", t)
|
||||
}
|
||||
JobParseError::MissingRequiredField(field) => {
|
||||
write!(f, "Missing required field: {}", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +63,7 @@ impl std::error::Error for JobError {
|
||||
#[async_trait::async_trait]
|
||||
pub trait Job: Send + Sync {
|
||||
/// The target type this job handles
|
||||
#[allow(dead_code)]
|
||||
fn target_type(&self) -> TargetType;
|
||||
|
||||
/// Process the job with the given API client and database pool
|
||||
@@ -99,14 +96,9 @@ impl JobType {
|
||||
}
|
||||
|
||||
/// Convert to a Job trait object
|
||||
pub fn as_job(self) -> Box<dyn Job> {
|
||||
pub fn boxed(self) -> Box<dyn Job> {
|
||||
match self {
|
||||
JobType::Subject(job) => Box::new(job),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to create a subject job
|
||||
pub fn create_subject_job(subject: String) -> JobType {
|
||||
JobType::Subject(subject::SubjectJob::new(subject))
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ impl Worker {
|
||||
.map_err(|e| JobError::Unrecoverable(anyhow::anyhow!(e)))?; // Parse errors are unrecoverable
|
||||
|
||||
// Get the job implementation
|
||||
let job_impl = job_type.as_job();
|
||||
let job_impl = job_type.boxed();
|
||||
|
||||
debug!(
|
||||
worker_id = self.id,
|
||||
|
||||
@@ -11,7 +11,8 @@ use axum::{
|
||||
use http::header;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use std::{collections::BTreeMap, sync::Arc, time::Duration};
|
||||
use std::{collections::BTreeMap, time::Duration};
|
||||
use tower_http::timeout::TimeoutLayer;
|
||||
use tower_http::{
|
||||
classify::ServerErrorsFailureClass,
|
||||
cors::{Any, CorsLayer},
|
||||
@@ -21,8 +22,6 @@ use tracing::{Span, debug, info, warn};
|
||||
|
||||
use crate::web::assets::{WebAssets, get_asset_metadata_cached};
|
||||
|
||||
use crate::banner::BannerApi;
|
||||
|
||||
/// Set appropriate caching headers based on asset type
|
||||
fn set_caching_headers(response: &mut Response, path: &str, etag: &str) {
|
||||
let headers = response.headers_mut();
|
||||
@@ -61,9 +60,7 @@ fn set_caching_headers(response: &mut Response, path: &str, etag: &str) {
|
||||
|
||||
/// Shared application state for web server
|
||||
#[derive(Clone)]
|
||||
pub struct BannerState {
|
||||
pub api: Arc<BannerApi>,
|
||||
}
|
||||
pub struct BannerState {}
|
||||
|
||||
/// Creates the web server router
|
||||
pub fn create_router(state: BannerState) -> Router {
|
||||
@@ -86,7 +83,7 @@ pub fn create_router(state: BannerState) -> Router {
|
||||
router = router.fallback(fallback);
|
||||
}
|
||||
|
||||
router.layer(
|
||||
router.layer((
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(|request: &Request<Body>| {
|
||||
tracing::debug_span!("request", path = request.uri().path())
|
||||
@@ -129,7 +126,8 @@ pub fn create_router(state: BannerState) -> Router {
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
TimeoutLayer::new(Duration::from_secs(10)),
|
||||
))
|
||||
}
|
||||
|
||||
/// Handler that extracts request information for caching
|
||||
|
||||
Reference in New Issue
Block a user