html terminal
Diffstat (limited to 'src/webhook.rs')
-rw-r--r--src/webhook.rs207
1 files changed, 207 insertions, 0 deletions
diff --git a/src/webhook.rs b/src/webhook.rs
new file mode 100644
index 0000000..275cdf9
--- /dev/null
+++ b/src/webhook.rs
@@ -0,0 +1,207 @@
+use std::{
+ sync::Arc,
+ time::{Duration, Instant},
+};
+
+use tokio::{
+ sync::broadcast::{self, error::TryRecvError},
+ task::JoinHandle,
+};
+use webhook::{client::WebhookClient, models::Message};
+
+macro_rules! send {
+ ($client:ident, $block:expr) => {{
+ let client = $client.clone();
+ let mut m = Message::new();
+ let mut bloc: Box<dyn FnMut(&mut Message) -> &mut Message> = Box::new($block);
+ bloc(&mut m);
+ m.allow_mentions(None, None, None, false);
+ tokio::spawn(async move {
+ client.send_message(&m).await.unwrap();
+ });
+ // handle goes out of scope, detatched
+ }};
+}
+
+pub struct Webhook(JoinHandle<()>);
+impl Webhook {
+ pub async fn new(mut stdout: broadcast::Receiver<String>, url: &str) -> Self {
+ let client = Arc::new(WebhookClient::new(url));
+ Self(tokio::spawn(async move {
+ define_print!("webhook");
+ let mut last: Option<Instant> = None;
+ let mut feed: Vec<String> = vec![];
+ loop {
+ let out = stdout.try_recv();
+ let now = Instant::now();
+ match out {
+ Err(e) => match e {
+ TryRecvError::Closed => fail!("closed"),
+ TryRecvError::Lagged(_) => continue,
+ TryRecvError::Empty => {
+ if let Some(earlier) = last {
+ let since = now.duration_since(earlier).as_secs();
+ if since > 2 || feed.len() > 15 {
+ last.take();
+ flush::<MindustryStyle>(feed, &client).await;
+ feed = vec![];
+ flush!();
+ }
+ }
+ async_std::task::sleep(Duration::from_millis(20)).await;
+ continue;
+ }
+ },
+ Ok(m) => {
+ for line in m.lines() {
+ feed.push(line.to_string());
+ input!("{line}");
+ }
+ last = Some(now);
+ }
+ }
+ async_std::task::sleep(Duration::from_millis(20)).await;
+ }
+ }))
+ }
+
+ pub fn running(&self) -> bool {
+ !self.0.is_finished()
+ }
+}
+
+/// functions ordered by call order
+pub trait OutputStyle {
+ /// first step
+ fn fix(raw_line: String) -> String;
+ /// ignore completely?
+ fn ignore(line: &str) -> bool;
+ /// is there no user
+ fn unnamed(line: &str) -> bool;
+ /// get the user and the content
+ fn split(line: &str) -> Option<(String, String)>;
+}
+
+macro_rules! s {
+ ($line:expr, $e:ident) => {
+ $line.starts_with(stringify!($e))
+ };
+ ($line:expr, $e:expr) => {
+ $line.starts_with($e)
+ };
+}
+pub struct MindustryStyle;
+impl OutputStyle for MindustryStyle {
+ fn fix(raw_line: String) -> String {
+ raw_line.split(' ').skip(3).collect::<Vec<&str>>().join(" ")
+ }
+ fn ignore(line: &str) -> bool {
+ line.trim().is_empty()
+ || line.contains("requested trace info")
+ || s!(line, Server)
+ || s!(line, Connected)
+ || s!(line, PLAGUE)
+ || s!(line, "1 mods loaded")
+ }
+
+ fn unnamed(line: &str) -> bool {
+ s!(line, [' ', '\t'])
+ }
+
+ fn split(line: &str) -> Option<(String, String)> {
+ if let Some(n) = line.find(':') {
+ if n < 12 {
+ if let Some((u, c)) = line.split_once(':') {
+ return Some((
+ unify(u).trim_start_matches('<').to_owned(),
+ unify(c).trim_end_matches('>').to_owned(),
+ ));
+ }
+ }
+ }
+ if let Some(index) = line.find("has") {
+ if line.contains("connected") {
+ let player = &line[..index];
+ return Some((unify(player).trim().to_owned(), "joined".to_owned()));
+ }
+ }
+ None
+ }
+}
+
+async fn flush<Style: OutputStyle>(feed: Vec<String>, client: &Arc<WebhookClient>) {
+ let mut current: Option<String> = None;
+ let mut message: Option<String> = None;
+ let mut unnamed: Option<String> = None;
+
+ // this code is very game dependent
+ for line in feed.into_iter() {
+ // [0-0-0-0 0-0-0-0] [i] ... -> ...
+ let line: String = Style::fix(line);
+ if Style::ignore(&line) {
+ continue;
+ }
+ if Style::unnamed(&line) {
+ unnamed.madd(line);
+ continue;
+ }
+
+ if let Some((name, msg)) = Style::split(&line) {
+ if !(msg.is_empty() || name.is_empty()) {
+ if let Some(n) = current.as_ref() {
+ if n == &name {
+ message.madd_panic(&msg);
+ continue;
+ } else {
+ let message = message.take().unwrap();
+ send!(client, |m| m.content(&message).username(n));
+ current.take();
+ }
+ }
+ current = Some(name.to_owned());
+ message = Some(msg.to_owned());
+ // interrupt
+ if let Some(msg) = unnamed.take() {
+ send!(client, |m| m.username("server").content(&msg));
+ }
+ continue;
+ }
+ }
+ unnamed.madd(line);
+ }
+ // leftovers
+ if let Some(n) = current.as_ref() {
+ let message = message.take().unwrap();
+ send!(client, |m| m.content(&message).username(n));
+ }
+ if let Some(msg) = unnamed.as_ref() {
+ send!(client, |m| m.username("server").content(msg));
+ }
+}
+/// latin > extended a > kill
+fn unify(s: &str) -> String {
+ s.chars()
+ .map(|c| if (c as u32) < 384 { c } else { ' ' })
+ .collect()
+}
+
+trait Madd {
+ fn madd(&mut self, line: String);
+ fn madd_panic(&mut self, line: &str);
+}
+
+// cant impl addassign because no impl for other types because
+impl Madd for Option<String> {
+ fn madd(&mut self, line: String) {
+ match self.take() {
+ Some(x) => *self = Some(x + "\n" + &line),
+ None => *self = Some(line),
+ }
+ }
+ fn madd_panic(&mut self, line: &str) {
+ match self.take() {
+ Some(x) => *self = Some(x + "\n" + line),
+ None => unreachable!(),
+ }
+ }
+}