Skip to content

Commit

Permalink
replace actix with axum
Browse files Browse the repository at this point in the history
  • Loading branch information
aki-mizu committed Apr 9, 2024
1 parent d8facdc commit ce2c63e
Show file tree
Hide file tree
Showing 8 changed files with 570 additions and 648 deletions.
986 changes: 435 additions & 551 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ readme = "README.md"
keywords = ["nostr", "api", "rest"]

[dependencies]
actix-cors = "0.6"
actix-web = "4.3"
axum = { version = "0.7", features = ["macros"] }
bincode = "1.3"
clap = { version = "4.1", features = ["derive"] }
dirs = "4.0"
env_logger = "0.10"
log = "0.4"
nostr-sdk = { version = "0.26", default-features = false }
redis = { version = "0.22", features = ["tokio-comp"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = "1.37"
toml = "0.7"
tower-http = { version = "0.5", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3" }
6 changes: 3 additions & 3 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use std::path::PathBuf;
use std::str::FromStr;

use clap::Parser;
use log::Level;
use nostr_sdk::Url;
use tracing::Level;

pub mod model;

Expand Down Expand Up @@ -49,8 +49,8 @@ impl Config {
toml::from_str(&content).expect("Impossible to parse config file");

let log_level: Level = match config_file.log_level {
Some(log_level) => Level::from_str(log_level.as_str()).unwrap_or(Level::Info),
None => Level::Info,
Some(log_level) => Level::from_str(log_level.as_str()).unwrap_or(Level::INFO),
None => Level::INFO,
};

Self {
Expand Down
2 changes: 1 addition & 1 deletion src/config/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub struct ConfigFileRedis {

#[derive(Debug, Clone)]
pub struct Config {
pub log_level: log::Level,
pub log_level: tracing::Level,
pub network: Network,
pub limit: Limit,
pub nostr: Nostr,
Expand Down
51 changes: 51 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) 2023 Nostr Development Kit Devs
// Distributed under the MIT software license

use axum::{
extract::{rejection::JsonRejection, FromRequest},
response::{IntoResponse, Response},
};
use serde_json::json;

#[derive(FromRequest)]
#[from_request(via(axum::Json), rejection(AppError))]
pub struct AppJson<T>(pub T);

impl<T> IntoResponse for AppJson<T>
where
axum::Json<T>: IntoResponse,
{
fn into_response(self) -> Response {
axum::Json(self.0).into_response()
}
}

pub enum AppError {
// The request body contained invalid JSON
JsonRejection(JsonRejection),
}

impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::JsonRejection(rejection) => (rejection.status(), rejection.body_text()),
};

(
status,
AppJson(json!({
"success": false,
"code": status.as_u16(),
"message": message,
"data": {}
})),
)
.into_response()
}
}

impl From<JsonRejection> for AppError {
fn from(rejection: JsonRejection) -> Self {
Self::JsonRejection(rejection)
}
}
80 changes: 42 additions & 38 deletions src/handler.rs
Original file line number Diff line number Diff line change
@@ -1,63 +1,67 @@
// Copyright (c) 2023 Nostr Development Kit Devs
// Distributed under the MIT software license

use actix_web::{get, post, web, HttpResponse};
use axum::{extract::State, response::Json};
use nostr_sdk::hashes::sha256::Hash as Sha256Hash;
use nostr_sdk::hashes::Hash;
use nostr_sdk::{Event, Filter};
use redis::AsyncCommands;
use serde_json::json;
use serde_json::{json, Value};

use crate::error::{AppError, AppJson};
use crate::AppState;

#[get("/ping")]
pub async fn ping() -> HttpResponse {
HttpResponse::Ok().json(json!({
pub async fn ping() -> Json<Value> {
Json(json!({
"success": true,
"message": "pong",
"data": {},
}))
}

#[post("/event")]
pub async fn publish_event(data: web::Data<AppState>, body: web::Json<Event>) -> HttpResponse {
pub async fn publish_event(
state: State<AppState>,
body: AppJson<Event>,
) -> Result<AppJson<Value>, AppError> {
let event: Event = body.0;

if let Err(e) = event.verify() {
return HttpResponse::BadRequest().json(json!({
return Ok(AppJson(json!({
"success": false,
"message": e.to_string(),
"data": {},
}));
})));
}

match data.client.send_event(event).await {
Ok(_) => HttpResponse::Ok().json(json!({
match state.client.send_event(event).await {
Ok(_) => Ok(AppJson(json!({
"success": true,
"message": "Event published",
"data": {},
})),
Err(e) => HttpResponse::BadRequest().json(json!({
}))),
Err(e) => Ok(AppJson(json!({
"success": false,
"message": e.to_string(),
"data": {},
})),
}))),
}
}

#[post("/events")]
pub async fn get_events(data: web::Data<AppState>, body: web::Json<Vec<Filter>>) -> HttpResponse {
pub async fn get_events(
state: State<AppState>,
body: AppJson<Vec<Filter>>,
) -> Result<AppJson<Value>, AppError> {
let filters: Vec<Filter> = body.0;

if filters.len() > data.config.limit.max_filters {
return HttpResponse::BadRequest().json(json!({
if filters.len() > state.config.limit.max_filters {
return Ok(AppJson(json!({
"success": false,
"message": format!("Too many filters (max allowed {})", data.config.limit.max_filters),
"message": format!("Too many filters (max allowed {})", state.config.limit.max_filters),
"data": {},
}));
})));
}

if let Some(redis) = &data.redis {
if let Some(redis) = &state.redis {
let mut connection = redis.get_async_connection().await.unwrap();
let hash: String = Sha256Hash::hash(format!("{filters:?}").as_bytes()).to_string();
match connection.exists::<&str, bool>(&hash).await {
Expand All @@ -67,59 +71,59 @@ pub async fn get_events(data: web::Data<AppState>, body: web::Json<Vec<Filter>>)
Ok(result) => {
let bytes: Vec<u8> = result;
let events: Vec<Event> = bincode::deserialize(&bytes).unwrap();
HttpResponse::Ok().json(json!({
Ok(AppJson(json!({
"success": true,
"message": format!("Got {} events", events.len()),
"data": events,
}))
})))
}
Err(e) => HttpResponse::BadRequest().json(json!({
Err(e) => Ok(AppJson(json!({
"success": false,
"message": e.to_string(),
"data": {},
})),
}))),
}
} else {
match data.client.get_events_of(filters, None).await {
match state.client.get_events_of(filters, None).await {
Ok(events) => {
let encoded: Vec<u8> = bincode::serialize(&events).unwrap();
let _: () = connection
.set_ex(hash, encoded, data.config.redis.expiration)
.set_ex(hash, encoded, state.config.redis.expiration)
.await
.unwrap();
HttpResponse::Ok().json(json!({
Ok(AppJson(json!({
"success": true,
"message": format!("Got {} events", events.len()),
"data": events,
}))
})))
}
Err(e) => HttpResponse::BadRequest().json(json!({
Err(e) => Ok(AppJson(json!({
"success": false,
"message": e.to_string(),
"data": {},
})),
}))),
}
}
}
Err(e) => HttpResponse::BadRequest().json(json!({
Err(e) => Ok(AppJson(json!({
"success": false,
"message": e.to_string(),
"data": {},
})),
}))),
}
} else {
// TODO: add a timeout
match data.client.get_events_of(filters, None).await {
Ok(events) => HttpResponse::Ok().json(json!({
match state.client.get_events_of(filters, None).await {
Ok(events) => Ok(AppJson(json!({
"success": true,
"message": format!("Got {} events", events.len()),
"data": events,
})),
Err(e) => HttpResponse::BadRequest().json(json!({
}))),
Err(e) => Ok(AppJson(json!({
"success": false,
"message": e.to_string(),
"data": {},
})),
}))),
}
}
}
10 changes: 4 additions & 6 deletions src/logger.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
// Copyright (c) 2023 Nostr Development Kit Devs
// Distributed under the MIT software license

use env_logger::{Builder, Env};
use log::Level;
use tracing::Level;

use super::Config;

pub fn init(config: &Config) {
let log_level: Level = if cfg!(debug_assertions) && config.log_level != Level::Trace {
Level::Debug
let log_level: Level = if cfg!(debug_assertions) && config.log_level != Level::TRACE {
Level::DEBUG
} else {
config.log_level
};

Builder::from_env(Env::default().default_filter_or(log_level.to_string())).init();
tracing_subscriber::fmt().with_max_level(log_level).init();
}
74 changes: 29 additions & 45 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
// Copyright (c) 2023 Nostr Development Kit Devs
// Distributed under the MIT software license

use actix_cors::Cors;
use actix_web::middleware::Logger;
use actix_web::{error, web, App, HttpResponse, HttpServer};
use axum::{
http::Method,
routing::{get, post},
Router,
};
use nostr_sdk::{Client, Keys, Result};
use redis::Client as RedisClient;
use serde_json::json;
use std::time::Duration;
use tower_http::cors::{Any, CorsLayer};
use tower_http::trace::TraceLayer;

mod config;
mod error;
mod handler;
mod logger;

use self::config::Config;

#[derive(Clone)]
pub struct AppState {
config: Config,
client: Client,
redis: Option<RedisClient>,
}

#[actix_web::main]
#[tokio::main]
async fn main() -> Result<()> {
let config = Config::get();

Expand All @@ -39,52 +45,30 @@ async fn main() -> Result<()> {
None
};

let data = web::Data::new(AppState {
let state = AppState {
config: config.clone(),
client,
redis,
});

let http_server = HttpServer::new(move || {
let json_config = web::JsonConfig::default().error_handler(|err, _req| {
error::InternalError::from_response(
"",
HttpResponse::BadRequest().json(json!({
"success": false,
"code": 400,
"message": err.to_string(),
"data": {}
})),
)
.into()
});
};

let cors = if config.network.permissive_cors {
Cors::permissive()
let app = Router::new()
.route("/ping", get(handler::ping))
.route("/publish_event", post(handler::publish_event))
.route("/events", post(handler::get_events))
.layer(if config.network.permissive_cors {
CorsLayer::permissive()
} else {
Cors::default()
.allowed_methods(vec!["GET", "POST"])
.allow_any_origin()
.max_age(3600)
};

App::new()
.wrap(Logger::default())
.wrap(cors)
.app_data(json_config)
.app_data(data.clone())
.configure(init_routes)
});
CorsLayer::new()
.allow_methods([Method::GET, Method::POST])
.allow_origin(Any)
.max_age(Duration::from_secs(3600))
})
.layer(TraceLayer::new_for_http())
.with_state(state);

let server = http_server.bind(config.network.listen_addr)?;
let listener = tokio::net::TcpListener::bind(config.network.listen_addr).await?;

log::info!("REST API listening on {}", config.network.listen_addr);

Ok(server.run().await?)
}
tracing::info!("REST API listening on {}", listener.local_addr()?);

fn init_routes(cfg: &mut web::ServiceConfig) {
cfg.service(handler::ping);
cfg.service(handler::publish_event);
cfg.service(handler::get_events);
Ok(axum::serve(listener, app).await?)
}

0 comments on commit ce2c63e

Please sign in to comment.