Finish up here
This commit is contained in:
parent
21c4b82658
commit
a1c1bc5809
59
Cargo.lock
generated
59
Cargo.lock
generated
@ -717,6 +717,17 @@ version = "2.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-terminal"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.10"
|
version = "1.0.10"
|
||||||
@ -765,6 +776,9 @@ name = "log"
|
|||||||
version = "0.4.21"
|
version = "0.4.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
@ -812,10 +826,13 @@ dependencies = [
|
|||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.2.0",
|
"hyper 1.2.0",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
|
"log",
|
||||||
"ring",
|
"ring",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serenity",
|
"serenity",
|
||||||
|
"stderrlog",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1280,6 +1297,15 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "skeptic"
|
name = "skeptic"
|
||||||
version = "0.13.7"
|
version = "0.13.7"
|
||||||
@ -1332,6 +1358,19 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stderrlog"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61c910772f992ab17d32d6760e167d2353f4130ed50e796752689556af07dc6b"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"is-terminal",
|
||||||
|
"log",
|
||||||
|
"termcolor",
|
||||||
|
"thread_local",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@ -1405,6 +1444,15 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termcolor"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.58"
|
version = "1.0.58"
|
||||||
@ -1425,6 +1473,16 @@ dependencies = [
|
|||||||
"syn 2.0.48",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thread_local"
|
||||||
|
version = "1.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.34"
|
version = "0.3.34"
|
||||||
@ -1483,6 +1541,7 @@ dependencies = [
|
|||||||
"mio",
|
"mio",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
|
@ -7,7 +7,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hyper = { version = "1", features = ["server", "http1", "http2"] }
|
hyper = { version = "1", features = ["server", "http1", "http2"] }
|
||||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
|
||||||
http-body-util = "0.1"
|
http-body-util = "0.1"
|
||||||
hyper-util = { version = "0.1", features = ["tokio"] }
|
hyper-util = { version = "0.1", features = ["tokio"] }
|
||||||
serde = {version = "1.0", features = ["derive"]}
|
serde = {version = "1.0", features = ["derive"]}
|
||||||
@ -16,3 +16,6 @@ chrono = { version = "0.4.35", features = ["serde"] }
|
|||||||
serenity = { version = "0.12" }
|
serenity = { version = "0.12" }
|
||||||
ring = { version = "0.17.8" }
|
ring = { version = "0.17.8" }
|
||||||
data-encoding = "2.5"
|
data-encoding = "2.5"
|
||||||
|
stderrlog = "0.6.0"
|
||||||
|
log = { version = "0.4.21", features = ["serde"] }
|
||||||
|
thiserror = "1.0.37"
|
@ -1,11 +1,11 @@
|
|||||||
use std::net::IpAddr;
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub host: IpAddr,
|
pub host: IpAddr,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
|
pub log_level: log::Level,
|
||||||
pub discord_token: String,
|
pub discord_token: String,
|
||||||
pub miniflux_base_url: String,
|
pub miniflux_base_url: String,
|
||||||
pub miniflux_webhook_secret: String,
|
pub miniflux_webhook_secret: String,
|
||||||
|
82
src/discord.rs
Normal file
82
src/discord.rs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::config::Config;
|
||||||
|
use super::miniflux_requests::{Entry, Feed, MinifluxEvent, NewEntries};
|
||||||
|
use serenity::all::{
|
||||||
|
Cache, Color, CreateButton, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, CreateMessage,
|
||||||
|
Http, UserId,
|
||||||
|
};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DiscordHolder {
|
||||||
|
pub cache: Arc<Cache>,
|
||||||
|
pub http: Arc<Http>,
|
||||||
|
pub config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum DiscordError {
|
||||||
|
#[error("Discord API error: {0}")]
|
||||||
|
DiscordApiError(#[from] serenity::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiscordHolder {
|
||||||
|
pub async fn send(&self, userid: u64, event: MinifluxEvent) -> Result<(), DiscordError> {
|
||||||
|
let user = self.http.get_user(UserId::new(userid)).await?;
|
||||||
|
|
||||||
|
match event {
|
||||||
|
MinifluxEvent::New(NewEntries { feed, entries }) => {
|
||||||
|
for entry in entries {
|
||||||
|
user.direct_message(
|
||||||
|
(&self.cache, self.http.as_ref()),
|
||||||
|
self.message_from_entry(&entry, &feed)?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
log::info!("Entry {} sent to user {}", entry.id, user.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message_from_entry(
|
||||||
|
&self,
|
||||||
|
entry: &Entry,
|
||||||
|
feed: &Feed,
|
||||||
|
) -> Result<CreateMessage, DiscordError> {
|
||||||
|
let author = CreateEmbedAuthor::new(&feed.title).url(&feed.site_url);
|
||||||
|
let footer = CreateEmbedFooter::new(format!("{} minutes", entry.reading_time.to_string()));
|
||||||
|
|
||||||
|
let miniflux_url =
|
||||||
|
format!("{}/feed/{}/entry/{}", self.config.miniflux_base_url, feed.id, entry.id);
|
||||||
|
|
||||||
|
let mut embed = CreateEmbed::new()
|
||||||
|
.title(&entry.title)
|
||||||
|
.url(&entry.url)
|
||||||
|
.footer(footer)
|
||||||
|
.timestamp(entry.published_at)
|
||||||
|
.author(author)
|
||||||
|
.color(Color::from_rgb(
|
||||||
|
(feed.id % 256) as u8,
|
||||||
|
((feed.id * feed.id) % 256) as u8,
|
||||||
|
((feed.id * feed.id * feed.id) % 256) as u8,
|
||||||
|
))
|
||||||
|
.description(&entry.content.chars().take(200).collect::<String>());
|
||||||
|
|
||||||
|
if entry.tags.len() > 0 {
|
||||||
|
embed = embed.field("Tags", entry.tags.join(","), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(enclosure) = entry.enclosures.iter().find(|e| e.mime_type.starts_with("image/"))
|
||||||
|
{
|
||||||
|
embed = embed.image(&enclosure.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
let external_button = CreateButton::new_link(&entry.url).label("external").emoji('📤');
|
||||||
|
let miniflux_button = CreateButton::new_link(miniflux_url).label("miniflux").emoji('📩');
|
||||||
|
|
||||||
|
Ok(CreateMessage::new().embed(embed).button(external_button).button(miniflux_button))
|
||||||
|
}
|
||||||
|
}
|
230
src/main.rs
230
src/main.rs
@ -1,26 +1,24 @@
|
|||||||
use std::{fs, net::SocketAddr};
|
use std::{convert::Infallible, fs, net::SocketAddr};
|
||||||
|
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use http_body_util::{combinators::BoxBody, BodyExt, Full};
|
use http_body_util::{BodyExt, Full};
|
||||||
use hyper::{
|
use hyper::{
|
||||||
body::{self, Body, Bytes},
|
body::{self, Body, Bytes},
|
||||||
server::conn::http1,
|
server::conn::http1,
|
||||||
service::service_fn,
|
service::service_fn,
|
||||||
Error, Request, Response,
|
Method, Request, Response, StatusCode,
|
||||||
};
|
};
|
||||||
use hyper_util::rt::TokioIo;
|
use hyper_util::rt::TokioIo;
|
||||||
use miniflux_requests::{Entry, Feed, MinifluxEvent, NewEntries};
|
use miniflux_requests::MinifluxEvent;
|
||||||
use ring::hmac;
|
use ring::hmac;
|
||||||
use serenity::{
|
use serenity::{all::GatewayIntents, Client};
|
||||||
all::{
|
use thiserror::Error;
|
||||||
CacheHttp, Color, CreateButton, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter,
|
|
||||||
CreateMessage, EmbedMessageBuilding, GatewayIntents, MessageBuilder, User, UserId,
|
|
||||||
},
|
|
||||||
Client,
|
|
||||||
};
|
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
use crate::discord::DiscordHolder;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod discord;
|
||||||
mod miniflux_requests;
|
mod miniflux_requests;
|
||||||
|
|
||||||
//FIXME!
|
//FIXME!
|
||||||
@ -30,155 +28,177 @@ const CONFIG_PATH: &'static str = "./config.json";
|
|||||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let config: Config = serde_json::from_slice(&fs::read(CONFIG_PATH).unwrap()).unwrap();
|
let config: Config = serde_json::from_slice(&fs::read(CONFIG_PATH).unwrap()).unwrap();
|
||||||
|
|
||||||
|
stderrlog::new()
|
||||||
|
.module(module_path!())
|
||||||
|
.verbosity(config.log_level)
|
||||||
|
.timestamp(stderrlog::Timestamp::Second)
|
||||||
|
.init()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let mut client = Client::builder(&config.discord_token, GatewayIntents::empty())
|
let mut client = Client::builder(&config.discord_token, GatewayIntents::empty())
|
||||||
.await
|
.await
|
||||||
.expect("Err creating client");
|
.expect("Error creating Discord client");
|
||||||
|
|
||||||
let addr = SocketAddr::from((config.host, config.port));
|
let addr = SocketAddr::from((config.host, config.port));
|
||||||
let listener = TcpListener::bind(addr).await?;
|
let listener = TcpListener::bind(addr).await?;
|
||||||
|
|
||||||
|
log::info!("Binded to {}:{}", config.host, config.port);
|
||||||
|
|
||||||
let cache = client.cache.clone();
|
let cache = client.cache.clone();
|
||||||
let http = client.http.clone();
|
let http = client.http.clone();
|
||||||
|
|
||||||
|
let holder = DiscordHolder { cache, http, config };
|
||||||
|
|
||||||
let client_handle = tokio::task::spawn(async move {
|
let client_handle = tokio::task::spawn(async move {
|
||||||
client.start().await.unwrap();
|
log::info!("Discord client started, should show up online now!");
|
||||||
|
client.start().await.expect("Failed starting Discord client!");
|
||||||
});
|
});
|
||||||
|
|
||||||
let loop_handle = tokio::task::spawn(async move {
|
let loop_handle = tokio::task::spawn(async move {
|
||||||
|
log::info!("Listening to TCP/HTTP connections now..");
|
||||||
loop {
|
loop {
|
||||||
let (stream, _) = listener.accept().await.unwrap();
|
let Ok((stream, addr)) = listener.accept().await else {
|
||||||
|
log::warn!("Failed to accept TCP stream");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
log::debug!("=> {}", addr);
|
||||||
|
|
||||||
let io = TokioIo::new(stream);
|
let io = TokioIo::new(stream);
|
||||||
|
|
||||||
let cache = cache.clone();
|
let holder = holder.clone();
|
||||||
let http = http.clone();
|
|
||||||
let config = config.clone();
|
|
||||||
|
|
||||||
// Spawn a tokio task to serve multiple connections concurrently
|
// Spawn a tokio task to serve multiple connections concurrently
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
if let Err(err) = http1::Builder::new()
|
let service = service_fn(|req: Request<body::Incoming>| {
|
||||||
// `service_fn` converts our function in a `Service`
|
let holder = holder.clone();
|
||||||
.serve_connection(
|
async move {
|
||||||
io,
|
Ok::<_, Infallible>(
|
||||||
service_fn(|req: Request<body::Incoming>| {
|
process_webhook(req, &holder).await.unwrap_or_else(|e| e.into()),
|
||||||
let cache = cache.clone();
|
|
||||||
let http = http.clone();
|
|
||||||
let config = config.clone();
|
|
||||||
async move { hello(req, (&cache, http.as_ref()), &config).await }
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
.await
|
}
|
||||||
{
|
});
|
||||||
println!("Error serving connection: {:?}", err);
|
let result = http1::Builder::new().serve_connection(io, service).await;
|
||||||
|
if let Err(err) = result {
|
||||||
|
log::warn!("Error serving connection {}: {:?}", addr, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let (res1, res2) = tokio::join!(client_handle, loop_handle);
|
let (res1, res2) = tokio::join!(client_handle, loop_handle);
|
||||||
res1.unwrap();
|
res1.expect("Failed unwrapping client handle result!");
|
||||||
res2.unwrap();
|
res2.expect("Failed unwrapping loop handle result!");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn hello(
|
async fn process_webhook(
|
||||||
req: Request<body::Incoming>,
|
req: Request<body::Incoming>,
|
||||||
ctx: impl CacheHttp + Copy,
|
holder: &DiscordHolder,
|
||||||
config: &Config,
|
) -> Result<Response<Full<Bytes>>, CustomError> {
|
||||||
) -> Result<Response<BoxBody<hyper::body::Bytes, hyper::Error>>, Error> {
|
let method = req.method();
|
||||||
// todo check method
|
if method != Method::POST {
|
||||||
let _method = req.method();
|
Err(CustomError::InvalidMethod(method.clone()))?;
|
||||||
let headers = req.headers();
|
|
||||||
|
|
||||||
// todo fix unwrap
|
|
||||||
let userid = req.uri().path().split("/").nth(1).unwrap().parse::<u64>().unwrap();
|
|
||||||
|
|
||||||
if !config.whitelisted_user_ids.contains(&userid) {
|
|
||||||
// Fixme!
|
|
||||||
panic!("{} not allowed!", userid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = ctx.http().get_user(UserId::new(userid)).await.unwrap();
|
let userid: u64 =
|
||||||
|
req.uri().path().split("/").nth(1).ok_or(CustomError::IdPathNotFound)?.parse()?;
|
||||||
|
|
||||||
|
if !holder.config.whitelisted_user_ids.contains(&userid) {
|
||||||
|
Err(CustomError::NotWhitelistedUser(userid))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers = req.headers();
|
||||||
|
|
||||||
// Todo remove expect
|
|
||||||
// Todo make sure contents match signature
|
|
||||||
let signature_bytes =
|
let signature_bytes =
|
||||||
headers.get("x-miniflux-signature").expect("expected signature").as_bytes();
|
headers.get("x-miniflux-signature").ok_or(CustomError::SignatureMissing)?.as_bytes();
|
||||||
let signature = data_encoding::HEXLOWER.decode(signature_bytes).unwrap();
|
let signature = data_encoding::HEXLOWER.decode(signature_bytes)?;
|
||||||
let event_type = headers.get("x-miniflux-event-type").expect("expected event type").clone();
|
let event_type =
|
||||||
|
headers.get("x-miniflux-event-type").ok_or(CustomError::EventTypeMissing)?.clone();
|
||||||
|
|
||||||
let upper = req.body().size_hint().upper().unwrap_or(u64::MAX);
|
let upper = req.body().size_hint().upper().unwrap_or(u64::MAX);
|
||||||
if upper > config.payload_max_size {
|
if upper > holder.config.payload_max_size {
|
||||||
let mut resp = Response::new(full("Body too big"));
|
Err(CustomError::PayloadTooLarge(upper))?;
|
||||||
*resp.status_mut() = hyper::StatusCode::PAYLOAD_TOO_LARGE;
|
|
||||||
dbg!("Got message, too big!");
|
|
||||||
return Ok(resp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let whole_body = req.collect().await?.to_bytes();
|
let whole_body = req.collect().await?.to_bytes();
|
||||||
let bytes = whole_body.iter().cloned().collect::<Vec<u8>>();
|
let bytes = whole_body.iter().cloned().collect::<Vec<u8>>();
|
||||||
|
|
||||||
let key = hmac::Key::new(hmac::HMAC_SHA256, &config.miniflux_webhook_secret.as_bytes());
|
let key = hmac::Key::new(hmac::HMAC_SHA256, &holder.config.miniflux_webhook_secret.as_bytes());
|
||||||
|
|
||||||
// TODO! Remove unwrap!
|
let () =
|
||||||
hmac::verify(&key, &bytes, &signature).unwrap();
|
hmac::verify(&key, &bytes, &signature).map_err(|_| CustomError::HmacValidationError)?;
|
||||||
|
|
||||||
let event: MinifluxEvent = serde_json::from_slice(&bytes).unwrap();
|
let event: MinifluxEvent = serde_json::from_slice(&bytes)?;
|
||||||
match event {
|
match event {
|
||||||
MinifluxEvent::New(_) => assert!(event_type == "new_entries"),
|
MinifluxEvent::New(_) => assert!(event_type == "new_entries"),
|
||||||
MinifluxEvent::Save(_) => assert!(event_type == "save_entry"),
|
MinifluxEvent::Save(_) => assert!(event_type == "save_entry"),
|
||||||
}
|
}
|
||||||
|
|
||||||
send(user, ctx, event, config).await;
|
log::info!("received {} from miniflux!", event_type.to_str().unwrap_or_default());
|
||||||
|
|
||||||
Ok(Response::new(full(vec![])))
|
let res = holder.send(userid, event).await;
|
||||||
|
if let Err(err) = res {
|
||||||
|
log::error!("Error while trying to send discord message {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Response::new(Full::new(vec![].into())))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, hyper::Error> {
|
#[derive(Error, Debug)]
|
||||||
Full::new(chunk.into()).map_err(|never| match never {}).boxed()
|
pub enum CustomError {
|
||||||
|
#[error("id path not found")]
|
||||||
|
IdPathNotFound,
|
||||||
|
#[error("invalid method: {0}")]
|
||||||
|
InvalidMethod(Method),
|
||||||
|
#[error("parse int error: {0}")]
|
||||||
|
ParseIntError(#[from] std::num::ParseIntError),
|
||||||
|
#[error("{0} not allowed!")]
|
||||||
|
NotWhitelistedUser(u64),
|
||||||
|
#[error("signature missing")]
|
||||||
|
SignatureMissing,
|
||||||
|
#[error("event_type missing")]
|
||||||
|
EventTypeMissing,
|
||||||
|
#[error("signature decoding error: {0}")]
|
||||||
|
DecodeError(#[from] data_encoding::DecodeError),
|
||||||
|
#[error("payload too large ({0})")]
|
||||||
|
PayloadTooLarge(u64),
|
||||||
|
#[error("Hyper Error: {0}")]
|
||||||
|
HyperError(#[from] hyper::Error),
|
||||||
|
#[error("HMAC signature validation failed")]
|
||||||
|
HmacValidationError,
|
||||||
|
#[error("JSON deserialization failed: {0}")]
|
||||||
|
JsonError(#[from] serde_json::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send(user: User, ctx: impl CacheHttp + Copy, event: MinifluxEvent, config: &Config) {
|
impl Into<Response<Full<Bytes>>> for CustomError {
|
||||||
match event {
|
fn into(self) -> Response<Full<Bytes>> {
|
||||||
MinifluxEvent::New(NewEntries { feed, entries }) => {
|
match &self {
|
||||||
for entry in entries {
|
CustomError::JsonError(_) => log::error!("{}", self),
|
||||||
user.direct_message(ctx, message_from_entry(&entry, &feed, config)).await.unwrap();
|
CustomError::HmacValidationError => log::warn!("{}", self),
|
||||||
|
CustomError::HyperError(_) => log::warn!("{}", self),
|
||||||
|
CustomError::PayloadTooLarge(_) => log::warn!("{}", self),
|
||||||
|
CustomError::SignatureMissing => log::warn!("{}", self),
|
||||||
|
CustomError::EventTypeMissing => log::warn!("{}", self),
|
||||||
|
_ => log::debug!("{}", self),
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ => {}
|
let mut resp = Response::new(Full::from(Bytes::from(self.to_string())));
|
||||||
|
let status_code = match self {
|
||||||
|
CustomError::IdPathNotFound => StatusCode::NOT_FOUND,
|
||||||
|
CustomError::InvalidMethod(_) => StatusCode::METHOD_NOT_ALLOWED,
|
||||||
|
CustomError::ParseIntError(_) => StatusCode::BAD_REQUEST,
|
||||||
|
CustomError::NotWhitelistedUser(_) => StatusCode::FORBIDDEN,
|
||||||
|
CustomError::SignatureMissing => StatusCode::BAD_REQUEST,
|
||||||
|
CustomError::EventTypeMissing => StatusCode::BAD_REQUEST,
|
||||||
|
CustomError::DecodeError(_) => StatusCode::BAD_REQUEST,
|
||||||
|
CustomError::PayloadTooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE,
|
||||||
|
CustomError::HyperError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
CustomError::HmacValidationError => StatusCode::FORBIDDEN,
|
||||||
|
CustomError::JsonError(_) => StatusCode::BAD_REQUEST,
|
||||||
|
};
|
||||||
|
|
||||||
|
*resp.status_mut() = status_code;
|
||||||
|
resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn message_from_entry(entry: &Entry, feed: &Feed, config: &Config) -> CreateMessage {
|
|
||||||
let author = CreateEmbedAuthor::new(&feed.title).url(&feed.site_url);
|
|
||||||
let footer = CreateEmbedFooter::new(format!("{} minutes", entry.reading_time.to_string()));
|
|
||||||
|
|
||||||
let minreq_url = format!("{}/feed/{}/entry/{}", config.miniflux_base_url, feed.id, entry.id);
|
|
||||||
|
|
||||||
let mut embed = CreateEmbed::new()
|
|
||||||
.title(&entry.title)
|
|
||||||
.url(&entry.url)
|
|
||||||
.footer(footer)
|
|
||||||
.timestamp(entry.published_at)
|
|
||||||
.author(author)
|
|
||||||
.color(Color::from_rgb(
|
|
||||||
(feed.id % 256) as u8,
|
|
||||||
((feed.id * feed.id) % 256) as u8,
|
|
||||||
((feed.id * feed.id * feed.id) % 256) as u8,
|
|
||||||
))
|
|
||||||
.description(&entry.content.chars().take(200).collect::<String>());
|
|
||||||
|
|
||||||
if entry.tags.len() > 0 {
|
|
||||||
embed = embed.field("Tags", entry.tags.join(","), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(enclosure) = entry.enclosures.iter().find(|e| e.mime_type.starts_with("image/")) {
|
|
||||||
embed = embed.image(&enclosure.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
let external_button = CreateButton::new_link(&entry.url).label("external").emoji('📤');
|
|
||||||
let minreq_button = CreateButton::new_link(minreq_url).label("minreq").emoji('📩');
|
|
||||||
|
|
||||||
CreateMessage::new().embed(embed).button(external_button).button(minreq_button)
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user