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"
|
async-trait = "0.1"
|
||||||
regex = "1.6.0"
|
regex = "1.6.0"
|
||||||
hyper = { version = "0.14", features = ["server"] }
|
hyper = { version = "0.14", features = ["server"] }
|
||||||
|
ellipse = "0.2.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# debug IRC commands
|
# debug IRC commands
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::net::SocketAddr;
|
use std::{net::SocketAddr, collections::HashMap};
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
@ -39,4 +39,5 @@ pub struct BotConfig {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct HttpConfig {
|
pub struct HttpConfig {
|
||||||
pub listen: SocketAddr,
|
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
|
|
||||||
}
|
|
83
src/web/mod.rs
Normal file
83
src/web/mod.rs
Normal file
|
@ -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
|
||||||
|
}
|
97
src/web/parser.rs
Normal file
97
src/web/parser.rs
Normal file
|
@ -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 a new issue