Skip to content

Commit

Permalink
refactor: actix-web -> axum
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanccn committed Oct 4, 2024
1 parent 0cfce31 commit a3da8f0
Show file tree
Hide file tree
Showing 36 changed files with 335 additions and 558 deletions.
557 changes: 150 additions & 407 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ license = "AGPL-3.0-only"
publish = false

[dependencies]
actix-web = "4.9.0"
axum = "0.7.7"
bytesize = "1.3.0"
chrono = "0.4.38"
color-eyre = "0.6.3"
dotenvy = "0.15.7"
eyre = "0.6.12"
hickory-resolver = { version = "0.24.1", features = ["dns-over-https-rustls", "webpki-roots"] }
humansize = "2.1.3"
humantime = "2.1.0"
Expand All @@ -33,8 +34,8 @@ serde_json = "1.0.128"
sysinfo = "0.31.4"
tokio = { version = "1.40.0", features = ["full"] }
toml = "0.8.19"
tower-http = { version = "0.6.1", features = ["trace"] }
tracing = "0.1.40"
tracing-actix-web = "0.7.13"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

Expand Down
161 changes: 95 additions & 66 deletions src/api.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
use poise::serenity_prelude as serenity;

use actix_web::{get, head, middleware, post, web, App, HttpResponse, HttpServer, Responder};
use axum::{
extract::{Form, Path, Request, State},
http::StatusCode,
middleware,
response::{IntoResponse, Json, Response},
routing::{get, post, Router},
};

use serde_json::json;
use tracing_actix_web::TracingLogger;

use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use tokio::net::TcpListener;

use crate::utils::actix::ActixError;
use tower_http::trace::TraceLayer;
use tracing::info;

use crate::utils::axum::AxumResult;

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct ValfiskPresenceData {
pub status: serenity::OnlineStatus,
Expand All @@ -32,56 +41,60 @@ impl ValfiskPresenceData {
pub static PRESENCE_STORE: Lazy<RwLock<HashMap<serenity::UserId, ValfiskPresenceData>>> =
Lazy::new(|| RwLock::new(HashMap::new()));

#[tracing::instrument]
#[get("/")]
async fn route_ping() -> Result<impl Responder, ActixError> {
Ok(HttpResponse::Ok().json(json!({ "ok": true })))
async fn route_ping() -> impl IntoResponse {
(StatusCode::OK, Json(json!({ "ok": true })))
}

async fn route_ping_head() -> impl IntoResponse {
StatusCode::OK
}

#[tracing::instrument]
#[head("/")]
async fn route_ping_head() -> Result<impl Responder, ActixError> {
Ok(HttpResponse::Ok().finish())
async fn route_not_found() -> impl IntoResponse {
(StatusCode::NOT_FOUND, Json(json!({ "error": "Not found" })))
}

#[tracing::instrument]
#[get("/presence/{user}")]
async fn route_get_presence(path: web::Path<(u64,)>) -> Result<impl Responder, ActixError> {
let path = path.into_inner();
if path.0 == 0 {
return Ok(HttpResponse::BadRequest().json(json!({ "error": "User ID cannot be 0!" })));
async fn route_presence(Path(user_id): Path<u64>) -> AxumResult<Response> {
if user_id == 0 {
return Ok((
StatusCode::BAD_REQUEST,
Json(json!({ "error": "User ID cannot be 0" })),
)
.into_response());
}

let user_id = serenity::UserId::from(path.0);
let user_id = serenity::UserId::from(user_id);

let store = PRESENCE_STORE.read().unwrap();
let presence_data = store.get(&user_id).cloned();
drop(store);

presence_data.map_or_else(
|| Ok(HttpResponse::NotFound().json(json!({ "error": "User not found!" }))),
|presence_data| Ok(HttpResponse::Ok().json(presence_data)),
|| {
Ok((
StatusCode::NOT_FOUND,
Json(json!({ "error": "User not found" })),
)
.into_response())
},
|presence_data| Ok((StatusCode::OK, Json(presence_data)).into_response()),
)
}

#[tracing::instrument]
#[head("/presence/{user}")]
async fn route_get_presence_head(path: web::Path<(u64,)>) -> Result<impl Responder, ActixError> {
let path = path.into_inner();
if path.0 == 0 {
return Ok(HttpResponse::BadRequest().finish());
async fn route_presence_head(Path(user_id): Path<u64>) -> AxumResult<StatusCode> {
if user_id == 0 {
return Ok(StatusCode::BAD_REQUEST);
}

let user_id = serenity::UserId::from(path.0);
let user_id = serenity::UserId::from(user_id);

let store = PRESENCE_STORE.read().unwrap();
let presence_exists = store.contains_key(&user_id);
drop(store);

if presence_exists {
Ok(HttpResponse::Ok().finish())
Ok(StatusCode::OK)
} else {
Ok(HttpResponse::NotFound().finish())
Ok(StatusCode::NOT_FOUND)
}
}

Expand All @@ -103,17 +116,18 @@ struct KofiData {
timestamp: serenity::Timestamp,
}

#[tracing::instrument]
#[post("/ko-fi")]
async fn route_kofi_webhook(
app_data: web::Data<AppState>,
form: web::Form<KofiFormData>,
) -> Result<impl Responder, ActixError> {
State(state): State<Arc<AppState>>,
form: Form<KofiFormData>,
) -> AxumResult<(StatusCode, impl IntoResponse)> {
let data: KofiData = serde_json::from_str(&form.0.data)?;
let verification_token = std::env::var("KOFI_VERIFICATION_TOKEN")?;

if data.verification_token != verification_token {
return Ok(HttpResponse::Unauthorized().json(json!({ "error": "unauthorized" })));
return Ok((
StatusCode::UNAUTHORIZED,
Json(json!({ "error": "Unauthorized" })),
));
}

if data.is_public {
Expand All @@ -137,61 +151,76 @@ async fn route_kofi_webhook(

channel
.send_message(
&app_data.into_inner().serenity_http,
&state.serenity_http,
serenity::CreateMessage::default().embed(embed),
)
.await?;
}
}

Ok(HttpResponse::Ok().json(json!({ "ok": true })))
Ok((StatusCode::OK, Json(json!({ "ok": true }))))
}

#[derive(Debug)]
struct AppState {
serenity_http: Arc<serenity::Http>,
}

async fn security_middleware(request: Request, next: middleware::Next) -> Response {
let mut response = next.run(request).await;

let h = response.headers_mut();
h.insert(
"content-security-policy",
"default-src 'none'".parse().unwrap(),
);
h.insert("access-control-allow-origin", "*".parse().unwrap());
h.insert("cross-origin-opener-policy", "same-origin".parse().unwrap());
h.insert(
"cross-origin-resource-policy",
"same-origin".parse().unwrap(),
);
h.insert("origin-agent-cluster", "?1".parse().unwrap());
h.insert("referrer-policy", "no-referrer".parse().unwrap());
h.insert("x-content-type-options", "nosniff".parse().unwrap());
h.insert("x-dns-prefetch-control", "off".parse().unwrap());
h.insert("x-download-options", "noopen".parse().unwrap());
h.insert("x-frame-options", "DENY".parse().unwrap());
h.insert("x-permitted-cross-domain-policies", "none".parse().unwrap());
h.insert("x-xss-protection", "1; mode=block".parse().unwrap());

response
}

#[tracing::instrument(skip(serenity_http))]
pub async fn serve(serenity_http: Arc<serenity::Http>) -> color_eyre::eyre::Result<()> {
pub async fn serve(serenity_http: Arc<serenity::Http>) -> eyre::Result<()> {
#[cfg(debug_assertions)]
let default_host = "127.0.0.1";
#[cfg(not(debug_assertions))]
let default_host = "0.0.0.0";

let host = std::env::var("HOST").unwrap_or_else(|_| default_host.to_owned());
let port = std::env::var("PORT").map_or(Ok(8080), |v| v.parse::<u16>())?;

let app_state = web::Data::new(AppState { serenity_http });
let state = Arc::new(AppState { serenity_http });

info!("Started API server {}", format!("http://{host}:{port}"));

HttpServer::new(move || {
let security_middleware = middleware::DefaultHeaders::new()
.add(("access-control-allow-origin", "*"))
.add(("cross-origin-opener-policy", "same-origin"))
.add(("cross-origin-resource-policy", "same-origin"))
.add(("origin-agent-cluster", "?1"))
.add(("referrer-policy", "no-referrer"))
.add(("x-content-type-options", "nosniff"))
.add(("x-dns-prefetch-control", "off"))
.add(("x-download-options", "noopen"))
.add(("x-frame-options", "SAMEORIGIN"))
.add(("x-permitted-cross-domain-policies", "none"))
.add(("x-xss-protection", "1; mode=block"));

App::new()
.wrap(TracingLogger::default())
.wrap(security_middleware)
.app_data(app_state.clone())
.service(route_ping_head)
.service(route_ping)
.service(route_get_presence_head)
.service(route_get_presence)
.service(route_kofi_webhook)
})
.bind((host, port))?
.run()
.await?;
let listener = TcpListener::bind((host, port)).await?;

let app = Router::new()
.route("/", get(route_ping).head(route_ping_head))
.route(
"/presence/:user",
get(route_presence).head(route_presence_head),
)
.route("/ko-fi", post(route_kofi_webhook))
.fallback(route_not_found)
.layer(middleware::from_fn(security_middleware))
.layer(TraceLayer::new_for_http())
.with_state(state);

axum::serve(listener, app).await?;

Ok(())
}
2 changes: 1 addition & 1 deletion src/commands/fun/autoreply.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use color_eyre::eyre::{eyre, Result};
use eyre::{eyre, Result};
use poise::{
serenity_prelude::{CreateEmbed, Timestamp},
CreateReply,
Expand Down
2 changes: 1 addition & 1 deletion src/commands/fun/owo.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use color_eyre::eyre::Result;
use eyre::Result;

use crate::Context;

Expand Down
2 changes: 1 addition & 1 deletion src/commands/fun/shiggy.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use poise::{serenity_prelude as serenity, CreateReply};

use crate::{utils::error_handling::ValfiskError, Context};
use color_eyre::eyre::{Report, Result};
use eyre::{Report, Result};

#[derive(serde::Deserialize)]
struct SafebooruResponse {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/moderation/ban.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use color_eyre::eyre::Result;
use eyre::Result;
use poise::serenity_prelude as serenity;

use super::LOGS_CHANNEL;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/moderation/kick.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use color_eyre::eyre::Result;
use eyre::Result;
use poise::serenity_prelude as serenity;

use super::LOGS_CHANNEL;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/moderation/timeout.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use color_eyre::eyre::Result;
use eyre::Result;
use poise::serenity_prelude as serenity;

use super::LOGS_CHANNEL;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/useful/dig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use poise::{
ChoiceParameter, CreateReply,
};

use color_eyre::eyre::Result;
use eyre::Result;
use once_cell::sync::Lazy;

pub static RESOLVER: Lazy<TokioAsyncResolver> = Lazy::new(|| {
Expand Down
5 changes: 3 additions & 2 deletions src/commands/useful/lighthouse.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use color_eyre::eyre::Result;
use eyre::Result;
use std::{collections::HashMap, env, time::Duration};

use poise::{serenity_prelude as serenity, CreateReply};
Expand Down Expand Up @@ -66,7 +66,8 @@ pub async fn lighthouse(
.get(api_url)
.timeout(Duration::from_secs(60))
.send()
.await?;
.await?
.error_for_status()?;

let data: PagespeedResponse = resp.json().await?;

Expand Down
2 changes: 1 addition & 1 deletion src/commands/useful/self_timeout.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use color_eyre::eyre::{eyre, Result};
use eyre::{eyre, Result};
use poise::serenity_prelude as serenity;

use crate::Context;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/useful/suppress_embeds.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use color_eyre::eyre::Result;
use eyre::Result;
use poise::serenity_prelude as serenity;

use crate::Context;
Expand Down
6 changes: 3 additions & 3 deletions src/commands/useful/translate.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use color_eyre::eyre::Result;
use eyre::Result;
use poise::{serenity_prelude as serenity, CreateReply};

use crate::Context;
use crate::{reqwest_client::HTTP, Context};

#[derive(serde::Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -49,7 +49,7 @@ pub async fn translate(ctx: Context<'_>, message: serenity::Message) -> Result<(
.append_pair("source", "input")
.append_pair("q", &message.content);

let resp = crate::reqwest_client::HTTP.get(api_url).send().await?;
let resp = HTTP.get(api_url).send().await?.error_for_status()?;

let data: GoogleTranslateResponse = resp.json().await?;
let translation = data
Expand Down
2 changes: 1 addition & 1 deletion src/commands/utils/ping.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use color_eyre::eyre::Result;
use eyre::Result;

use crate::Context;

Expand Down
2 changes: 1 addition & 1 deletion src/commands/utils/presence.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{storage::presence::PresenceChoice, Context};
use poise::{serenity_prelude as serenity, CreateReply};

use color_eyre::eyre::Result;
use eyre::Result;
use tracing::info;

/// Modify the Discord presence shown by the bot
Expand Down
2 changes: 1 addition & 1 deletion src/commands/utils/say.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use color_eyre::eyre::Result;
use eyre::Result;
use poise::serenity_prelude as serenity;

use crate::Context;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/utils/sysinfo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use poise::{serenity_prelude::CreateEmbed, CreateReply};
use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System};

use crate::Context;
use color_eyre::eyre::Result;
use eyre::Result;

/// Get system information for the bot host
#[poise::command(slash_command, guild_only)]
Expand Down
Loading

0 comments on commit a3da8f0

Please sign in to comment.