mirror of
https://github.com/Xevion/banner.git
synced 2025-12-05 23:14:20 -06:00
feat: smart day string, terse refactor and use types properly, work on unimplemented commands lightly, util modules,
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
//! Main Banner API client implementation.
|
||||
|
||||
use crate::banner::{SessionManager, models::*, query::SearchQuery};
|
||||
use crate::banner::{models::*, query::SearchQuery, session::SessionManager, util::user_agent};
|
||||
use anyhow::{Context, Result};
|
||||
use axum::http::HeaderValue;
|
||||
use reqwest::Client;
|
||||
@@ -166,7 +166,7 @@ impl BannerApi {
|
||||
pub async fn get_campuses(
|
||||
&self,
|
||||
search: &str,
|
||||
term: i32,
|
||||
term: &str,
|
||||
offset: i32,
|
||||
max_results: i32,
|
||||
) -> Result<Vec<Pair>> {
|
||||
@@ -178,7 +178,7 @@ impl BannerApi {
|
||||
let url = format!("{}/classSearch/get_campus", self.base_url);
|
||||
let params = [
|
||||
("searchTerm", search),
|
||||
("term", &term.to_string()),
|
||||
("term", term),
|
||||
("offset", &offset.to_string()),
|
||||
("max", &max_results.to_string()),
|
||||
("uniqueSessionId", &session_id),
|
||||
@@ -205,10 +205,10 @@ impl BannerApi {
|
||||
pub async fn get_course_meeting_time(
|
||||
&self,
|
||||
term: &str,
|
||||
crn: i32,
|
||||
crn: &str,
|
||||
) -> Result<Vec<MeetingScheduleInfo>> {
|
||||
let url = format!("{}/searchResults/getFacultyMeetingTimes", self.base_url);
|
||||
let params = [("term", term), ("courseReferenceNumber", &crn.to_string())];
|
||||
let params = [("term", term), ("courseReferenceNumber", crn)];
|
||||
|
||||
let response = self
|
||||
.client
|
||||
@@ -242,14 +242,14 @@ impl BannerApi {
|
||||
));
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ResponseWrapper {
|
||||
fmt: Vec<MeetingTimeResponse>,
|
||||
}
|
||||
let response: MeetingTimesApiResponse =
|
||||
response.json().await.context("Failed to parse response")?;
|
||||
|
||||
let wrapper: ResponseWrapper = response.json().await.context("Failed to parse response")?;
|
||||
|
||||
Ok(wrapper.fmt.into_iter().map(|m| m.schedule_info()).collect())
|
||||
Ok(response
|
||||
.fmt
|
||||
.into_iter()
|
||||
.map(|m| m.schedule_info())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Performs a search for courses.
|
||||
@@ -357,10 +357,10 @@ impl BannerApi {
|
||||
}
|
||||
|
||||
/// Gets course details (placeholder - needs implementation).
|
||||
pub async fn get_course_details(&self, term: i32, crn: i32) -> Result<ClassDetails> {
|
||||
pub async fn get_course_details(&self, term: &str, crn: &str) -> Result<ClassDetails> {
|
||||
let body = serde_json::json!({
|
||||
"term": term.to_string(),
|
||||
"courseReferenceNumber": crn.to_string(),
|
||||
"term": term,
|
||||
"courseReferenceNumber": crn,
|
||||
"first": "first"
|
||||
});
|
||||
|
||||
@@ -382,11 +382,6 @@ impl BannerApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a browser-like user agent string.
|
||||
fn user_agent() -> &'static str {
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"
|
||||
}
|
||||
|
||||
/// Attempt to parse JSON and, on failure, include a contextual snippet around the error location
|
||||
fn parse_json_with_context<T: serde::de::DeserializeOwned>(body: &str) -> Result<T> {
|
||||
match serde_json::from_str::<T>(body) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(unused_imports)]
|
||||
|
||||
//! Banner API module for interacting with Ellucian Banner systems.
|
||||
//!
|
||||
//! This module provides functionality to:
|
||||
@@ -11,6 +13,7 @@ pub mod models;
|
||||
pub mod query;
|
||||
pub mod scraper;
|
||||
pub mod session;
|
||||
pub mod util;
|
||||
|
||||
pub use api::*;
|
||||
pub use models::*;
|
||||
|
||||
@@ -59,6 +59,24 @@ pub struct Course {
|
||||
pub meetings_faculty: Vec<MeetingTimeResponse>,
|
||||
}
|
||||
|
||||
impl Course {
|
||||
/// Returns the course title in the format "SUBJ #### - Course Title"
|
||||
pub fn display_title(&self) -> String {
|
||||
format!(
|
||||
"{} {} - {}",
|
||||
self.subject, self.course_number, self.course_title
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the name of the primary instructor, or "Unknown" if not available
|
||||
pub fn primary_instructor_name(&self) -> &str {
|
||||
self.faculty
|
||||
.first()
|
||||
.map(|f| f.display_name.as_str())
|
||||
.unwrap_or("Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
/// Class details (to be implemented)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClassDetails {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use bitflags::{Flags, bitflags};
|
||||
use chrono::{DateTime, NaiveDate, NaiveTime, Timelike, Utc};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::{cmp::Ordering, fmt::Display, str::FromStr};
|
||||
use std::{cmp::Ordering, collections::HashSet, fmt::Display, str::FromStr};
|
||||
|
||||
use super::terms::Term;
|
||||
|
||||
@@ -159,6 +159,19 @@ impl DayOfWeek {
|
||||
DayOfWeek::Sunday => "Su",
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to full string representation
|
||||
pub fn to_full_string(self) -> &'static str {
|
||||
match self {
|
||||
DayOfWeek::Monday => "Monday",
|
||||
DayOfWeek::Tuesday => "Tuesday",
|
||||
DayOfWeek::Wednesday => "Wednesday",
|
||||
DayOfWeek::Thursday => "Thursday",
|
||||
DayOfWeek::Friday => "Friday",
|
||||
DayOfWeek::Saturday => "Saturday",
|
||||
DayOfWeek::Sunday => "Sunday",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<MeetingDays> for DayOfWeek {
|
||||
@@ -423,18 +436,36 @@ impl MeetingScheduleInfo {
|
||||
}
|
||||
|
||||
/// Get formatted days string
|
||||
pub fn days_string(&self) -> String {
|
||||
pub fn days_string(&self) -> Option<String> {
|
||||
if self.days.is_empty() {
|
||||
"None".to_string()
|
||||
} else if self.days.is_all() {
|
||||
"Everyday".to_string()
|
||||
} else {
|
||||
self.days_of_week()
|
||||
.iter()
|
||||
.map(|day| day.to_short_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
return None;
|
||||
}
|
||||
if self.days.is_all() {
|
||||
return Some("Everyday".to_string());
|
||||
}
|
||||
|
||||
let days_of_week = self.days_of_week();
|
||||
if days_of_week.len() == 1 {
|
||||
return Some(days_of_week[0].to_full_string().to_string());
|
||||
}
|
||||
|
||||
// Mapper function to get the short string representation of the day of week
|
||||
let mapper = {
|
||||
let ambiguous = self.days.intersects(
|
||||
MeetingDays::Tuesday
|
||||
| MeetingDays::Thursday
|
||||
| MeetingDays::Saturday
|
||||
| MeetingDays::Sunday,
|
||||
);
|
||||
|
||||
if ambiguous {
|
||||
|day: &DayOfWeek| day.to_short_string().to_string()
|
||||
} else {
|
||||
|day: &DayOfWeek| day.to_short_string().chars().next().unwrap().to_string()
|
||||
}
|
||||
};
|
||||
|
||||
Some(days_of_week.iter().map(mapper).collect::<String>())
|
||||
}
|
||||
|
||||
/// Returns a formatted string representing the location of the meeting
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Session management for Banner API.
|
||||
|
||||
use crate::banner::util::user_agent;
|
||||
use anyhow::Result;
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use reqwest::Client;
|
||||
@@ -195,8 +196,3 @@ impl SessionManager {
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a browser-like user agent string
|
||||
fn user_agent() -> &'static str {
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"
|
||||
}
|
||||
|
||||
6
src/banner/util.rs
Normal file
6
src/banner/util.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
//! Utility functions for the Banner module.
|
||||
|
||||
/// Returns a browser-like user agent string.
|
||||
pub fn user_agent() -> &'static str {
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Google Calendar command implementation.
|
||||
|
||||
use crate::banner::{Course, DayOfWeek, MeetingScheduleInfo, Term};
|
||||
use crate::bot::{Context, Error};
|
||||
use crate::banner::{Course, DayOfWeek, MeetingScheduleInfo};
|
||||
use crate::bot::{Context, Error, utils};
|
||||
use chrono::NaiveDate;
|
||||
use std::collections::HashMap;
|
||||
use tracing::{error, info};
|
||||
@@ -18,34 +18,21 @@ pub async fn gcal(
|
||||
|
||||
ctx.defer().await?;
|
||||
|
||||
let app_state = &ctx.data().app_state;
|
||||
let banner_api = &app_state.banner_api;
|
||||
|
||||
// Get current term dynamically
|
||||
let current_term_status = Term::get_current();
|
||||
let term = current_term_status.inner();
|
||||
|
||||
// Fetch live course data from Redis cache via AppState
|
||||
let course = match app_state
|
||||
.get_course_or_fetch(&term.to_string(), &crn.to_string())
|
||||
.await
|
||||
{
|
||||
Ok(course) => course,
|
||||
Err(e) => {
|
||||
error!(%e, crn, "failed to fetch course data");
|
||||
return Err(Error::from(e));
|
||||
}
|
||||
};
|
||||
let course = utils::get_course_by_crn(&ctx, crn).await?;
|
||||
let term = course.term.clone();
|
||||
|
||||
// Get meeting times
|
||||
let meeting_times = match banner_api
|
||||
.get_course_meeting_time(&term.to_string(), crn)
|
||||
let meeting_times = match ctx
|
||||
.data()
|
||||
.app_state
|
||||
.banner_api
|
||||
.get_course_meeting_time(&term, &crn.to_string())
|
||||
.await
|
||||
{
|
||||
Ok(meeting_time) => meeting_time,
|
||||
Err(e) => {
|
||||
error!("failed to get meeting times: {}", e);
|
||||
return Err(Error::from(e));
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -74,8 +61,10 @@ pub async fn gcal(
|
||||
.map(|m| {
|
||||
let link = generate_gcal_url(&course, m)?;
|
||||
let detail = match &m.time_range {
|
||||
Some(range) => format!("{} {}", m.days_string(), range.format_12hr()),
|
||||
None => m.days_string(),
|
||||
Some(range) => {
|
||||
format!("{} {}", m.days_string().unwrap(), range.format_12hr())
|
||||
}
|
||||
None => m.days_string().unwrap(),
|
||||
};
|
||||
Ok(LinkDetail { link, detail })
|
||||
})
|
||||
@@ -104,10 +93,7 @@ fn generate_gcal_url(
|
||||
course: &Course,
|
||||
meeting_time: &MeetingScheduleInfo,
|
||||
) -> Result<String, anyhow::Error> {
|
||||
let course_text = format!(
|
||||
"{} {} - {}",
|
||||
course.subject, course.course_number, course.course_title
|
||||
);
|
||||
let course_text = course.display_title();
|
||||
|
||||
let dates_text = {
|
||||
let (start, end) = meeting_time.datetime_range();
|
||||
@@ -119,18 +105,14 @@ fn generate_gcal_url(
|
||||
};
|
||||
|
||||
// Get instructor name
|
||||
let instructor_name = if !course.faculty.is_empty() {
|
||||
&course.faculty[0].display_name
|
||||
} else {
|
||||
"Unknown"
|
||||
};
|
||||
let instructor_name = course.primary_instructor_name();
|
||||
|
||||
// The event description
|
||||
let details_text = format!(
|
||||
"CRN: {}\nInstructor: {}\nDays: {}",
|
||||
course.course_reference_number,
|
||||
instructor_name,
|
||||
meeting_time.days_string()
|
||||
meeting_time.days_string().unwrap()
|
||||
);
|
||||
|
||||
// The event location
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! ICS command implementation for generating calendar files.
|
||||
|
||||
use crate::bot::{Context, Error};
|
||||
use crate::bot::{Context, Error, utils};
|
||||
use tracing::info;
|
||||
|
||||
/// Generate an ICS file for a course
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
@@ -10,16 +11,15 @@ pub async fn ics(
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer().await?;
|
||||
|
||||
// TODO: Get BannerApi from context or global state
|
||||
// TODO: Get current term dynamically
|
||||
let term = 202510; // Hardcoded for now
|
||||
let course = utils::get_course_by_crn(&ctx, crn).await?;
|
||||
|
||||
// TODO: Implement actual ICS file generation
|
||||
ctx.say(format!(
|
||||
"ICS command not yet implemented - BannerApi integration needed\nCRN: {}, Term: {}",
|
||||
crn, term
|
||||
"ICS generation for '{}' is not yet implemented.",
|
||||
course.display_title()
|
||||
))
|
||||
.await?;
|
||||
|
||||
info!("ics command completed for CRN: {}", crn);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
//! Course search command implementation.
|
||||
|
||||
use crate::banner::SearchQuery;
|
||||
use crate::banner::{SearchQuery, Term};
|
||||
use crate::bot::{Context, Error};
|
||||
use anyhow::anyhow;
|
||||
use regex::Regex;
|
||||
use tracing::info;
|
||||
|
||||
/// Search for courses with various filters
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
@@ -40,12 +42,37 @@ pub async fn search(
|
||||
query = query.max_results(max_results.min(25)); // Cap at 25
|
||||
}
|
||||
|
||||
// TODO: Get current term dynamically
|
||||
// TODO: Get BannerApi from context or global state
|
||||
// For now, we'll return an error
|
||||
ctx.say("Search functionality not yet implemented - BannerApi integration needed")
|
||||
let term = Term::get_current().inner().to_string();
|
||||
let search_result = ctx
|
||||
.data()
|
||||
.app_state
|
||||
.banner_api
|
||||
.search(&term, &query, "subjectDescription", false)
|
||||
.await?;
|
||||
|
||||
let response = if let Some(courses) = search_result.data {
|
||||
if courses.is_empty() {
|
||||
"No courses found with the specified criteria.".to_string()
|
||||
} else {
|
||||
courses
|
||||
.iter()
|
||||
.map(|course| {
|
||||
format!(
|
||||
"**{}**: {} ({})",
|
||||
course.display_title(),
|
||||
course.primary_instructor_name(),
|
||||
course.course_reference_number
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
} else {
|
||||
"No courses found with the specified criteria.".to_string()
|
||||
};
|
||||
|
||||
ctx.say(response).await?;
|
||||
info!("search command completed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -65,22 +92,24 @@ fn parse_course_code(input: &str) -> Result<(i32, i32), Error> {
|
||||
};
|
||||
|
||||
if low > high {
|
||||
return Err("Invalid range: low value greater than high value".into());
|
||||
return Err(anyhow!(
|
||||
"Invalid range: low value greater than high value"
|
||||
));
|
||||
}
|
||||
|
||||
if low < 1000 || high > 9999 {
|
||||
return Err("Course codes must be between 1000 and 9999".into());
|
||||
return Err(anyhow!("Course codes must be between 1000 and 9999"));
|
||||
}
|
||||
|
||||
return Ok((low, high));
|
||||
}
|
||||
return Err("Invalid range format".into());
|
||||
return Err(anyhow!("Invalid range format"));
|
||||
}
|
||||
|
||||
// Handle wildcard format (e.g, "34xx")
|
||||
if input.contains('x') {
|
||||
if input.len() != 4 {
|
||||
return Err("Wildcard format must be exactly 4 characters".into());
|
||||
return Err(anyhow!("Wildcard format must be exactly 4 characters"));
|
||||
}
|
||||
|
||||
let re = Regex::new(r"(\d+)(x+)").unwrap();
|
||||
@@ -92,22 +121,22 @@ fn parse_course_code(input: &str) -> Result<(i32, i32), Error> {
|
||||
let high = low + 10_i32.pow(x_count as u32) - 1;
|
||||
|
||||
if low < 1000 || high > 9999 {
|
||||
return Err("Course codes must be between 1000 and 9999".into());
|
||||
return Err(anyhow!("Course codes must be between 1000 and 9999"));
|
||||
}
|
||||
|
||||
return Ok((low, high));
|
||||
}
|
||||
return Err("Invalid wildcard format".into());
|
||||
return Err(anyhow!("Invalid wildcard format"));
|
||||
}
|
||||
|
||||
// Handle single course code
|
||||
if input.len() == 4 {
|
||||
let code: i32 = input.parse()?;
|
||||
if !(1000..=9999).contains(&code) {
|
||||
return Err("Course codes must be between 1000 and 9999".into());
|
||||
return Err(anyhow!("Course codes must be between 1000 and 9999"));
|
||||
}
|
||||
return Ok((code, code));
|
||||
}
|
||||
|
||||
Err("Invalid course code format".into())
|
||||
Err(anyhow!("Invalid course code format"))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! Terms command implementation.
|
||||
|
||||
use crate::banner::{BannerTerm, Term};
|
||||
use crate::bot::{Context, Error};
|
||||
use tracing::info;
|
||||
|
||||
/// List available terms or search for a specific term
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
@@ -13,14 +15,40 @@ pub async fn terms(
|
||||
|
||||
let search_term = search.unwrap_or_default();
|
||||
let page_number = page.unwrap_or(1).max(1);
|
||||
let max_results = 10;
|
||||
|
||||
// TODO: Get BannerApi from context or global state
|
||||
// For now, we'll return a placeholder response
|
||||
ctx.say(format!(
|
||||
"Terms command not yet implemented - BannerApi integration needed\nSearch: '{}', Page: {}",
|
||||
search_term, page_number
|
||||
))
|
||||
.await?;
|
||||
let terms = ctx
|
||||
.data()
|
||||
.app_state
|
||||
.banner_api
|
||||
.get_terms(&search_term, page_number, max_results)
|
||||
.await?;
|
||||
|
||||
let response = if terms.is_empty() {
|
||||
"No terms found.".to_string()
|
||||
} else {
|
||||
let current_term_code = Term::get_current().inner().to_string();
|
||||
terms
|
||||
.iter()
|
||||
.map(|term| format_term(term, ¤t_term_code))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
};
|
||||
|
||||
ctx.say(response).await?;
|
||||
info!("terms command completed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_term(term: &BannerTerm, current_term_code: &str) -> String {
|
||||
let is_current = if term.code == current_term_code {
|
||||
" (current)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let is_archived = if term.is_archived() { " (archived)" } else { "" };
|
||||
format!(
|
||||
"- `{}`: {}{}{}",
|
||||
term.code, term.description, is_current, is_archived
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Time command implementation for course meeting times.
|
||||
|
||||
use crate::bot::{Context, Error};
|
||||
use crate::bot::{utils, Context, Error};
|
||||
use tracing::info;
|
||||
|
||||
/// Get meeting times for a specific course
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
@@ -10,16 +11,15 @@ pub async fn time(
|
||||
) -> Result<(), Error> {
|
||||
ctx.defer().await?;
|
||||
|
||||
// TODO: Get BannerApi from context or global state
|
||||
// TODO: Get current term dynamically
|
||||
let term = 202510; // Hardcoded for now
|
||||
let course = utils::get_course_by_crn(&ctx, crn).await?;
|
||||
|
||||
// TODO: Implement actual meeting time retrieval
|
||||
// TODO: Implement actual meeting time retrieval and display
|
||||
ctx.say(format!(
|
||||
"Time command not yet implemented - BannerApi integration needed\nCRN: {}, Term: {}",
|
||||
crn, term
|
||||
"Meeting time display for '{}' is not yet implemented.",
|
||||
course.display_title()
|
||||
))
|
||||
.await?;
|
||||
|
||||
info!("time command completed for CRN: {}", crn);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use crate::app_state::AppState;
|
||||
use crate::error::Error;
|
||||
|
||||
pub mod commands;
|
||||
pub mod utils;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Data {
|
||||
pub app_state: AppState,
|
||||
} // User data, which is stored and accessible in all command invocations
|
||||
pub type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
pub type Context<'a> = poise::Context<'a, Data, Error>;
|
||||
|
||||
/// Get all available commands
|
||||
|
||||
24
src/bot/utils.rs
Normal file
24
src/bot/utils.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
//! Bot command utilities.
|
||||
|
||||
use crate::banner::{Course, Term};
|
||||
use crate::bot::Context;
|
||||
use crate::error::Result;
|
||||
use tracing::error;
|
||||
|
||||
/// Gets a course by its CRN for the current term.
|
||||
pub async fn get_course_by_crn(ctx: &Context<'_>, crn: i32) -> Result<Course> {
|
||||
let app_state = &ctx.data().app_state;
|
||||
|
||||
// Get current term dynamically
|
||||
let current_term_status = Term::get_current();
|
||||
let term = current_term_status.inner();
|
||||
|
||||
// Fetch live course data from Redis cache via AppState
|
||||
app_state
|
||||
.get_course_or_fetch(&term.to_string(), &crn.to_string())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(%e, crn, "failed to fetch course data");
|
||||
e
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//! Application-specific error types.
|
||||
|
||||
pub type Error = anyhow::Error;
|
||||
pub type Result<T, E = Error> = anyhow::Result<T, E>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod app_state;
|
||||
pub mod banner;
|
||||
pub mod bot;
|
||||
pub mod error;
|
||||
pub mod services;
|
||||
pub mod web;
|
||||
|
||||
@@ -18,6 +18,7 @@ mod app_state;
|
||||
mod banner;
|
||||
mod bot;
|
||||
mod config;
|
||||
mod error;
|
||||
mod services;
|
||||
mod web;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::Service;
|
||||
use crate::web::{BannerState, create_banner_router};
|
||||
use crate::web::{BannerState, create_router};
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::broadcast;
|
||||
@@ -30,7 +30,7 @@ impl Service for WebService {
|
||||
|
||||
async fn run(&mut self) -> Result<(), anyhow::Error> {
|
||||
// Create the main router with Banner API routes
|
||||
let app = create_banner_router(self.banner_state.clone());
|
||||
let app = create_router(self.banner_state.clone());
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], self.port));
|
||||
info!(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use axum::{Router, extract::State, response::Json, routing::get};
|
||||
use serde_json::{Value, json};
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, info};
|
||||
use tracing::info;
|
||||
|
||||
/// Shared application state for web server
|
||||
#[derive(Clone)]
|
||||
@@ -13,7 +13,7 @@ pub struct BannerState {
|
||||
}
|
||||
|
||||
/// Creates the web server router
|
||||
pub fn create_banner_router(state: BannerState) -> Router {
|
||||
pub fn create_router(state: BannerState) -> Router {
|
||||
Router::new()
|
||||
.route("/", get(root))
|
||||
.route("/health", get(health))
|
||||
@@ -22,9 +22,7 @@ pub fn create_banner_router(state: BannerState) -> Router {
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
/// Root endpoint - shows API info
|
||||
async fn root() -> Json<Value> {
|
||||
debug!("root endpoint accessed");
|
||||
Json(json!({
|
||||
"message": "Banner Discord Bot API",
|
||||
"version": "0.1.0",
|
||||
|
||||
Reference in New Issue
Block a user