Skip to content
This repository has been archived by the owner on Oct 6, 2024. It is now read-only.

Commit

Permalink
add basic action log logging
Browse files Browse the repository at this point in the history
  • Loading branch information
katsumi143 committed Dec 24, 2023
1 parent f3e870b commit b7f34ca
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 82 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions mellow/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ rust-version = "1.73"
[dependencies]
hex = "0.4.3"
log = "0.4.20"
hmac = "0.12.1"
sha2 = "0.10.8"
tokio = { version = "1.34.0", features = ["full"] }
serde = { version = "1.0.193", features = ["derive"] }
chrono = "0.4.31"
Expand Down
6 changes: 3 additions & 3 deletions mellow/src/commands/syncing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use tokio::time;
use mellow_macros::command;

use crate::{
server::{ ServerLog, send_logs },
server::ServerLog,
discord::{ DiscordMember, get_members, edit_original_response },
syncing::{ RoleChangeKind, SyncMemberResult, sync_member, create_sign_up, get_connection_metadata, sync_single_user },
database::{ UserResponse, get_server, get_users_by_discord },
Expand Down Expand Up @@ -54,7 +54,7 @@ pub async fn sync_with_token(user: UserResponse, member: DiscordMember, guild_id
}).await;

if result.profile_changed {
send_logs(&result.server, vec![ServerLog::ServerProfileSync {
result.server.send_logs(vec![ServerLog::ServerProfileSync {
member,
forced_by: None,
role_changes: result.role_changes.clone(),
Expand Down Expand Up @@ -125,7 +125,7 @@ pub async fn forcesyncall(interaction: InteractionPayload) -> SlashResponse {
}).await;

if !logs.is_empty() {
send_logs(&server, logs).await;
server.send_logs(logs).await;
}
});
SlashResponse::DeferMessage
Expand Down
15 changes: 9 additions & 6 deletions mellow/src/http/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
use actix_web::{
web,
http::{ StatusCode, header::ContentType },
middleware::Logger,
App, HttpServer, HttpResponse
};
use derive_more::{ Error, Display };

mod routes;
pub mod routes;

pub async fn start() -> std::io::Result<()> {
HttpServer::new(||
Expand All @@ -21,6 +20,9 @@ pub async fn start() -> std::io::Result<()> {

#[derive(Debug, Display, Error)]
pub enum ApiError {
#[display(fmt = "internal_error")]
InternalError,

#[display(fmt = "invalid_request")]
GenericInvalidRequest,

Expand All @@ -44,21 +46,22 @@ impl actix_web::error::ResponseError for ApiError {
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code())
.insert_header(ContentType::json())
.body(format!(r#"
.body(format!(r#"{{
"error": "{}"
"#, self.to_string()))
}}"#, self.to_string()))
}

fn status_code(&self) -> StatusCode {
match *self {
ApiError::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
ApiError::GenericInvalidRequest => StatusCode::BAD_REQUEST,
ApiError::InvalidApiKey |
ApiError::InvalidSignature => StatusCode::FORBIDDEN,
ApiError::UserNotFound |
ApiError::SignUpNotFound => StatusCode::NOT_FOUND,
ApiError::NotImplemented => StatusCode::NOT_IMPLEMENTED
ApiError::NotImplemented => StatusCode::NOT_IMPLEMENTED,
}
}
}

pub type ApiResult<T> = actix_web::Result<web::Json<T>, ApiError>;
pub type ApiResult<T> = actix_web::Result<T, ApiError>;
82 changes: 72 additions & 10 deletions mellow/src/http/routes.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use serde::Deserialize;
use sha2::Sha256;
use hmac::{ Mac, Hmac };
use serde::{ Serialize, Deserialize };
use once_cell::sync::Lazy;
use actix_web::{
Responder, HttpRequest, HttpResponse,
Expand All @@ -8,12 +10,15 @@ use ed25519_dalek::{ Verifier, Signature, VerifyingKey, SignatureError };

use super::{ ApiError, ApiResult };
use crate::{
server::ServerLog,
discord::get_member,
syncing::{ SyncMemberResult, SIGN_UPS, sync_single_user },
database::get_users_by_discord,
database,
interaction
};

type HmacSha256 = Hmac<Sha256>;

const API_KEY: &str = env!("API_KEY");
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");

Expand All @@ -22,12 +27,17 @@ const PUBLIC_KEY: Lazy<VerifyingKey> = Lazy::new(||
.map(|vec| VerifyingKey::from_bytes(&vec.try_into().unwrap()).unwrap())
.unwrap()
);
const ABSOLUTESOLVER: &[u8] = env!("ABSOLUTESOLVER").as_bytes();

pub fn configure(cfg: &mut web::ServiceConfig) {
cfg
.service(index)
.service(interactions)
.service(sync_member);
.service(sync_member)
.service(
web::scope("/absolutesolver")
.service(action_log_webhook)
);
}

#[get("/")]
Expand All @@ -36,7 +46,7 @@ async fn index() -> impl Responder {
}

#[post("/interactions")]
async fn interactions(request: HttpRequest, body: String) -> ApiResult<interaction::InteractionResponse> {
async fn interactions(request: HttpRequest, body: String) -> ApiResult<web::Json<interaction::InteractionResponse>> {
let headers = request.headers();
let signature = headers.get("x-signature-ed25519")
.and_then(|x| x.to_str().ok())
Expand All @@ -59,11 +69,11 @@ struct SyncMemberPayload {
}

#[post("/server/{server_id}/member/{user_id}/sync")]
async fn sync_member(request: HttpRequest, body: web::Json<SyncMemberPayload>, path: web::Path<(String, String)>) -> ApiResult<SyncMemberResult> {
async fn sync_member(request: HttpRequest, body: web::Json<SyncMemberPayload>, path: web::Path<(String, String)>) -> ApiResult<web::Json<SyncMemberResult>> {
// TODO: make this... easier on the eyes.
if request.headers().get("x-api-key").map_or(false, |x| x.to_str().unwrap() == API_KEY.to_string()) {
let (server_id, user_id) = path.into_inner();
if let Some(user) = get_users_by_discord(vec![user_id.clone()], server_id.clone()).await.into_iter().next() {
if let Some(user) = database::get_users_by_discord(vec![user_id.clone()], server_id.clone()).await.into_iter().next() {
let member = get_member(&server_id, &user_id).await;
return Ok(web::Json(if let Some(token) = &body.webhook_token {
crate::commands::syncing::sync_with_token(user, member, &server_id, &token).await
Expand All @@ -82,11 +92,63 @@ async fn sync_member(request: HttpRequest, body: web::Json<SyncMemberPayload>, p
} else { Err(ApiError::InvalidApiKey) }
}

#[derive(Deserialize, Serialize)]
pub struct ActionLogAuthor {
pub id: String,
pub name: Option<String>,
pub username: String
}

impl ActionLogAuthor {
pub fn display_name(&self) -> String {
self.name.as_ref().map_or_else(|| self.username.clone(), |x| x.clone())
}
}

#[derive(Deserialize, Serialize)]
pub struct ActionLogWebhookPayload {
#[serde(rename = "type")]
pub kind: String,
pub author: ActionLogAuthor,
pub server_id: String
}

#[post("/supabase_webhooks/action_log")]
async fn action_log_webhook(request: HttpRequest, body: String) -> ApiResult<HttpResponse> {
absolutesolver(&request, &body)?;

let payload: ActionLogWebhookPayload = serde_json::from_str(&body)
.map_err(|_| ApiError::GenericInvalidRequest)?;

database::get_server(&payload.server_id)
.await
.send_logs(vec![ServerLog::ActionLog(payload)])
.await;

Ok(HttpResponse::Ok().finish())
}

fn absolutesolver(request: &HttpRequest, body: impl ToString) -> Result<(), ApiError> {
let mut mac = HmacSha256::new_from_slice(ABSOLUTESOLVER)
.map_err(|_| ApiError::InternalError)?;
mac.update(body.to_string().as_bytes());

mac.verify_slice(
request.headers()
.get("absolutesolver")
.ok_or(ApiError::InvalidSignature)
.map(|x| hex::decode(x))?
.map_err(|_| ApiError::InvalidSignature)?
.as_slice()
)
.map_err(|_| ApiError::InvalidSignature)
}

fn verify_interaction_body(body: impl Into<String>, signature: impl Into<String>, timestamp: impl Into<String>) -> Result<(), SignatureError> {
PUBLIC_KEY.verify(
format!("{}{}", timestamp.into(), body.into()).as_bytes(),
&hex::decode(signature.into())
.map(|vec| Signature::from_bytes(&vec.try_into().unwrap()))
format!("{}{}", timestamp.into(), body.into()).as_bytes(),
&hex::decode(signature.into())
.map(|vec| Signature::from_bytes(&vec.try_into().unwrap()))
.unwrap()
)
)
}
2 changes: 1 addition & 1 deletion mellow/src/interaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ pub struct InteractionResponse {
data: Option<InteractionResponseData>
}

pub async fn handle_request(body: String) -> ApiResult<InteractionResponse> {
pub async fn handle_request(body: String) -> ApiResult<Json<InteractionResponse>> {
let payload: InteractionPayload = serde_json::from_str(&body).unwrap();
match payload.kind {
InteractionKind::Ping => Ok(Json(InteractionResponse {
Expand Down
Loading

0 comments on commit b7f34ca

Please sign in to comment.