Add basic discord webhook emulator
This commit is contained in:
parent
c7ce19275a
commit
8c898caabd
|
@ -27,6 +27,7 @@ irc = { version = "0.15", default-features = false, features = ["tls-rust"] }
|
|||
async-trait = "0.1"
|
||||
regex = "1.6.0"
|
||||
hyper = { version = "0.14", features = ["server"] }
|
||||
ellipse = "0.2.0"
|
||||
|
||||
[features]
|
||||
# debug IRC commands
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::net::SocketAddr;
|
||||
use std::{net::SocketAddr, collections::HashMap};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
|
@ -39,4 +39,5 @@ pub struct BotConfig {
|
|||
#[derive(Deserialize)]
|
||||
pub struct HttpConfig {
|
||||
pub listen: SocketAddr,
|
||||
pub webhooks: HashMap<String, String>
|
||||
}
|
||||
|
|
49
src/web.rs
49
src/web.rs
|
@ -1,49 +0,0 @@
|
|||
use std::{convert::Infallible, sync::Arc};
|
||||
|
||||
use hyper::{
|
||||
service::{make_service_fn, service_fn},
|
||||
Body, Request, Response, Server, StatusCode,
|
||||
};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::config::HttpConfig;
|
||||
|
||||
pub struct HttpContext<SF>
|
||||
where
|
||||
SF: Fn(String, String) -> anyhow::Result<()>,
|
||||
{
|
||||
pub cfg: HttpConfig,
|
||||
pub sendmsg: SF,
|
||||
}
|
||||
|
||||
async fn handle<SF>(_ctx: Arc<HttpContext<SF>>, _req: Request<Body>) -> anyhow::Result<Response<Body>>
|
||||
where
|
||||
SF: Fn(String, String) -> anyhow::Result<()> + Send + Sync + 'static,
|
||||
{
|
||||
let resp = Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(Body::empty())?;
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
pub async fn run<SF>(context: HttpContext<SF>, mut shutdown: broadcast::Receiver<()>) -> hyper::Result<()>
|
||||
where
|
||||
SF: Fn(String, String) -> anyhow::Result<()> + Send + Sync + 'static,
|
||||
{
|
||||
let ctx = Arc::new(context);
|
||||
let make_service = make_service_fn({
|
||||
let ctx = ctx.clone();
|
||||
move |_conn| {
|
||||
let ctx = ctx.clone();
|
||||
let service = service_fn(move |req| handle(ctx.clone(), req));
|
||||
async move { Ok::<_, Infallible>(service) }
|
||||
}
|
||||
});
|
||||
|
||||
let server = Server::bind(&ctx.cfg.listen).serve(make_service);
|
||||
server
|
||||
.with_graceful_shutdown(async {
|
||||
shutdown.recv().await.unwrap();
|
||||
})
|
||||
.await
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
use std::{convert::Infallible, sync::Arc};
|
||||
|
||||
use hyper::{
|
||||
header::HeaderValue,
|
||||
service::{make_service_fn, service_fn},
|
||||
Body, Request, Response, Server, StatusCode, body::to_bytes,
|
||||
};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::config::HttpConfig;
|
||||
|
||||
mod parser;
|
||||
|
||||
pub struct HttpContext<SF>
|
||||
where
|
||||
SF: Fn(String, String) -> anyhow::Result<()>,
|
||||
{
|
||||
pub cfg: HttpConfig,
|
||||
pub sendmsg: SF,
|
||||
}
|
||||
|
||||
async fn handle<SF>(ctx: Arc<HttpContext<SF>>, req: Request<Body>) -> anyhow::Result<Response<Body>>
|
||||
where
|
||||
SF: Fn(String, String) -> anyhow::Result<()> + Send + Sync + 'static,
|
||||
{
|
||||
let mime = req
|
||||
.headers()
|
||||
.get("Content-Type")
|
||||
.map(HeaderValue::to_str)
|
||||
.transpose()?;
|
||||
if let Some(mime) = mime {
|
||||
if mime != "application/json" {
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(Body::from("wrong content-type"))?);
|
||||
}
|
||||
} else {
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(Body::from("no content-type"))?);
|
||||
}
|
||||
let webhook = (&req.uri().path()[1..]).to_string();
|
||||
let channel = if let Some(c) = ctx.cfg.webhooks.get(&webhook) {
|
||||
c
|
||||
} else {
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::from("webhook path not registered"))?);
|
||||
};
|
||||
let body_bytes = to_bytes(req.into_body()).await?;
|
||||
let body = String::from_utf8_lossy(&body_bytes);
|
||||
let response = parser::textify(&body, &webhook)?;
|
||||
(ctx.sendmsg)(channel.to_string(), response)?;
|
||||
let resp = Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(Body::empty())?;
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
pub async fn run<SF>(
|
||||
context: HttpContext<SF>,
|
||||
mut shutdown: broadcast::Receiver<()>,
|
||||
) -> hyper::Result<()>
|
||||
where
|
||||
SF: Fn(String, String) -> anyhow::Result<()> + Send + Sync + 'static,
|
||||
{
|
||||
let ctx = Arc::new(context);
|
||||
let make_service = make_service_fn({
|
||||
let ctx = ctx.clone();
|
||||
move |_conn| {
|
||||
let ctx = ctx.clone();
|
||||
let service = service_fn(move |req| handle(ctx.clone(), req));
|
||||
async move { Ok::<_, Infallible>(service) }
|
||||
}
|
||||
});
|
||||
|
||||
let server = Server::bind(&ctx.cfg.listen).serve(make_service);
|
||||
server
|
||||
.with_graceful_shutdown(async {
|
||||
shutdown.recv().await.unwrap();
|
||||
})
|
||||
.await
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
use ellipse::Ellipse;
|
||||
use serde::Deserialize;
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct WebhookData {
|
||||
content: Option<String>,
|
||||
username: Option<String>,
|
||||
embeds: Vec<Embed>
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Embed {
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
url: Option<String>,
|
||||
timestamp: Option<String>,
|
||||
footer: Option<EmbedFooter>,
|
||||
image: Option<UrlObject>,
|
||||
thumbnail: Option<UrlObject>,
|
||||
video: Option<UrlObject>,
|
||||
author: Option<EmbedAuthor>,
|
||||
fields: Vec<EmbedField>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UrlObject {
|
||||
url: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EmbedAuthor {
|
||||
name: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EmbedFooter {
|
||||
text: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EmbedField {
|
||||
name: String,
|
||||
value: String
|
||||
}
|
||||
|
||||
pub fn textify(json: &str, webhook_name: &str) -> anyhow::Result<String> {
|
||||
let wh: WebhookData = serde_json::from_str(json)?;
|
||||
let mut buf = format!("-- [Webhook: {}]\r\n", wh.username.as_deref().unwrap_or(webhook_name));
|
||||
|
||||
if let Some(content) = wh.content {
|
||||
let content = content.trim().truncate_ellipse(450);
|
||||
for line in content.lines() {
|
||||
write!(&mut buf, " {}\r\n", line)?;
|
||||
}
|
||||
}
|
||||
for embed in wh.embeds {
|
||||
write!(&mut buf, "-> {}\r\n", embed.title.as_deref().unwrap_or("Embed"))?;
|
||||
if let Some(description) = embed.description {
|
||||
let description = description.trim().truncate_ellipse(450);
|
||||
for line in description.lines() {
|
||||
write!(&mut buf, " {}\r\n", line)?;
|
||||
}
|
||||
}
|
||||
for field in embed.fields {
|
||||
write!(&mut buf, " + {}\r\n", field.name)?;
|
||||
let value = field.value.trim().truncate_ellipse(450);
|
||||
for line in value.lines() {
|
||||
write!(&mut buf, " {}\r\n", line)?;
|
||||
}
|
||||
}
|
||||
if let Some(url) = embed.url {
|
||||
write!(&mut buf, " url: {}\r\n", url)?;
|
||||
}
|
||||
if let Some(image) = embed.image {
|
||||
write!(&mut buf, " img: {}\r\n", image.url)?;
|
||||
}
|
||||
if let Some(thumbnail) = embed.thumbnail {
|
||||
write!(&mut buf, " thumb: {}\r\n", thumbnail.url)?;
|
||||
}
|
||||
if let Some(video) = embed.video {
|
||||
write!(&mut buf, " vid: {}\r\n", video.url)?;
|
||||
}
|
||||
if let Some(author) = embed.author {
|
||||
write!(&mut buf, " by: {}\r\n", author.name)?;
|
||||
}
|
||||
if let Some(footer) = embed.footer {
|
||||
write!(&mut buf, " - {}\r\n", footer.text)?;
|
||||
}
|
||||
if let Some(timestamp) = embed.timestamp {
|
||||
write!(&mut buf, " - {}\r\n", timestamp)?;
|
||||
}
|
||||
}
|
||||
|
||||
buf.push_str("-- end of webhook");
|
||||
Ok(buf)
|
||||
}
|
Loading…
Reference in New Issue