smokestack: init
This commit is contained in:
parent
24f6c2afad
commit
0d179e3624
|
@ -312,6 +312,18 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "caveman-smokestack"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"cave",
|
||||||
|
"futures",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.74"
|
version = "1.0.74"
|
||||||
|
|
|
@ -3,4 +3,5 @@ members = [
|
||||||
"cave",
|
"cave",
|
||||||
"hunter",
|
"hunter",
|
||||||
"gatherer",
|
"gatherer",
|
||||||
|
"smokestack",
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "caveman-smokestack"
|
||||||
|
description = "telnet server"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
futures = "0.3"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
# serde_yaml = "0.9"
|
||||||
|
# chrono = "0.4"
|
||||||
|
# redis = { version = "0.22", features = ["tokio-comp", "connection-manager"] }
|
||||||
|
log = "0.4"
|
||||||
|
cave = { path = "../cave" }
|
||||||
|
# hyper = { version = "0.14", features = ["stream"] }
|
||||||
|
# axum = "0.5"
|
||||||
|
# axum-macros = "0.2"
|
||||||
|
# axum-extra = { version = "0.3", features = ["spa"] }
|
||||||
|
# askama = "0.11"
|
|
@ -0,0 +1,4 @@
|
||||||
|
#redis: redis://10.233.12.2:6379/
|
||||||
|
redis: redis://127.0.0.1:6378/
|
||||||
|
|
||||||
|
listen_port: 8023
|
|
@ -0,0 +1,5 @@
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub redis: String,
|
||||||
|
pub listen_port: u16,
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
RwLock,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use futures::{Stream, StreamExt};
|
||||||
|
use tokio::{
|
||||||
|
io::AsyncWriteExt,
|
||||||
|
net::TcpListener,
|
||||||
|
sync::mpsc::{channel, Receiver, Sender},
|
||||||
|
};
|
||||||
|
use cave::{
|
||||||
|
config::LoadConfig,
|
||||||
|
feed::Post,
|
||||||
|
firehose::FirehoseFactory,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
|
||||||
|
fn html_to_text(html: &str) -> String {
|
||||||
|
let mut result = String::with_capacity(html.len());
|
||||||
|
let mut in_tag = false;
|
||||||
|
let mut entity = None;
|
||||||
|
for c in html.chars() {
|
||||||
|
if c == '<' {
|
||||||
|
// tag open
|
||||||
|
in_tag = true;
|
||||||
|
} else if in_tag && c == '>' {
|
||||||
|
// tag close
|
||||||
|
in_tag = false;
|
||||||
|
} else if in_tag {
|
||||||
|
// ignore
|
||||||
|
} else if c == '&' {
|
||||||
|
entity = Some(String::with_capacity(5));
|
||||||
|
} else if entity.is_some() && c == ';' {
|
||||||
|
let r = match entity.take().unwrap().as_str() {
|
||||||
|
"amp" => "&",
|
||||||
|
"lt" => "<",
|
||||||
|
"gt" => ">",
|
||||||
|
"quot" => "\"",
|
||||||
|
"apos" => "\'",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
result.push_str(r);
|
||||||
|
} else if let Some(entity) = entity.as_mut() {
|
||||||
|
entity.push(c);
|
||||||
|
} else {
|
||||||
|
result.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_message(post: Post) -> Option<String> {
|
||||||
|
let language = &post.language?;
|
||||||
|
let time = &post.created_at;
|
||||||
|
let display_name = &post.account.display_name;
|
||||||
|
let username = &post.account.username;
|
||||||
|
let host = post.account.host()?;
|
||||||
|
let text = html_to_text(&post.content);
|
||||||
|
Some(format!(
|
||||||
|
"[{}] {} {} <@{}@{}>\r\n{}\r\n\r\n",
|
||||||
|
language,
|
||||||
|
time,
|
||||||
|
display_name,
|
||||||
|
username,
|
||||||
|
host,
|
||||||
|
text,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct State {
|
||||||
|
next_id: Arc<RwLock<usize>>,
|
||||||
|
consumers: Arc<RwLock<HashMap<usize, Sender<Arc<Vec<u8>>>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
State {
|
||||||
|
next_id: Arc::new(RwLock::new(0)),
|
||||||
|
consumers: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_next_id(&self) -> usize {
|
||||||
|
let mut next_id = self.next_id.write().unwrap();
|
||||||
|
let result = *next_id;
|
||||||
|
*next_id += 1;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pipe(&self) -> Pipe {
|
||||||
|
let (tx, rx) = channel(1);
|
||||||
|
|
||||||
|
let id = self.get_next_id();
|
||||||
|
let mut consumers = self.consumers.write().unwrap();
|
||||||
|
consumers.insert(id, tx);
|
||||||
|
|
||||||
|
Pipe {
|
||||||
|
id, rx,
|
||||||
|
consumers: self.consumers.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn broadcast(&self, msg: Arc<Vec<u8>>) {
|
||||||
|
let txs = {
|
||||||
|
let consumers = self.consumers.read().unwrap();
|
||||||
|
consumers.values()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
for tx in txs {
|
||||||
|
let _ = tx.send(msg.clone()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Pipe {
|
||||||
|
id: usize,
|
||||||
|
pub rx: Receiver<Arc<Vec<u8>>>,
|
||||||
|
consumers: Arc<RwLock<HashMap<usize, Sender<Arc<Vec<u8>>>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Pipe {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
log::trace!("drop pipe");
|
||||||
|
let mut consumers = self.consumers.write().unwrap();
|
||||||
|
consumers.remove(&self.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn publisher(state: State, firehose: impl Stream<Item = Vec<u8>>) {
|
||||||
|
firehose.for_each(move |data| {
|
||||||
|
let state = state.clone();
|
||||||
|
async move {
|
||||||
|
let post = serde_json::from_slice(&data)
|
||||||
|
.ok();
|
||||||
|
let msg = post.and_then(format_message);
|
||||||
|
if let Some(msg) = msg {
|
||||||
|
state.broadcast(Arc::new(msg.into_bytes())).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
cave::init::exit_on_panic();
|
||||||
|
cave::init::init_logger();
|
||||||
|
|
||||||
|
let config = config::Config::load();
|
||||||
|
|
||||||
|
let state = State::new();
|
||||||
|
|
||||||
|
let firehose_factory = FirehoseFactory::new(config.redis);
|
||||||
|
let firehose = firehose_factory.produce()
|
||||||
|
.await
|
||||||
|
.expect("firehose")
|
||||||
|
.filter_map(|item| async move { item.ok() });
|
||||||
|
tokio::spawn(
|
||||||
|
publisher(state.clone(), firehose)
|
||||||
|
);
|
||||||
|
|
||||||
|
let listener = TcpListener::bind(
|
||||||
|
format!("[::]:{}", config.listen_port)
|
||||||
|
).await.expect("TcpListener::bind");
|
||||||
|
|
||||||
|
cave::systemd::ready();
|
||||||
|
|
||||||
|
while let Ok((mut socket, addr)) = listener.accept().await {
|
||||||
|
log::info!("Accepted connection from {:?}", addr);
|
||||||
|
let mut pipe = state.pipe();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
log::trace!("while...");
|
||||||
|
while let Some(msg) = pipe.rx.recv().await {
|
||||||
|
match socket.write_all(&msg[..]).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue