From 35ce3ad7719cf8d21b8ed2d446d2495887633899 Mon Sep 17 00:00:00 2001 From: PotentialStyx <62217716+PotentialStyx@users.noreply.github.com> Date: Sat, 27 Apr 2024 17:08:48 -0700 Subject: [PATCH] feat(services, goval-ident): Implement chat variant of `goval-ident` protocol - Fully implements [`goval-ident`](https://govaldocs.pages.dev/service/goval-ident/) - Makes `goval-ident` a disable-able feature - Unifies implemention of `chat` and `gcsfiles` variants --- Cargo.toml | 3 +- services/Cargo.toml | 6 +- services/src/gcsfiles.rs | 18 ++-- services/src/goval_ident.rs | 131 +++++++++++++++++++++++++++++ services/src/lib.rs | 26 +++++- services/src/types/channel_info.rs | 4 +- services/src/types/fs_watcher.rs | 4 +- services/src/types/messaging.rs | 4 +- services/src/types/mod.rs | 3 + services/src/types/server_info.rs | 46 ++++++++++ src/goval_server.rs | 5 +- src/main.rs | 7 +- src/server_info.rs | 16 ++++ 13 files changed, 245 insertions(+), 28 deletions(-) create mode 100644 services/src/goval_ident.rs create mode 100644 services/src/types/server_info.rs create mode 100644 src/server_info.rs diff --git a/Cargo.toml b/Cargo.toml index d5fe515..0b2b06d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,12 +17,13 @@ See https://govaldocs.pages.dev""" members = [".", "migration", "entity", "services", "protobuf"] [features] -default = ["replspace", "database", "repldb", "verify_connections"] +default = ["replspace", "database", "repldb", "verify_connections", "goval-ident"] repldb = ["database"] database = ["dep:sea-orm", "dep:sea-query", "dep:migration", "dep:entity"] replspace = [] fun-stuff = ["dep:chrono", "dep:chrono-tz"] verify_connections = ["dep:hyper", "dep:hyper-tls", "dep:hyper-util", "dep:http-body-util"] +goval-ident = ["homeval_services/goval-ident"] [dependencies] goval = { path = "protobuf", package = "protobuf" } diff --git a/services/Cargo.toml b/services/Cargo.toml index c220aa4..f3f78cb 100644 --- a/services/Cargo.toml +++ b/services/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +goval-ident = ["dep:serde_json"] [dependencies] anyhow = "1.0.81" @@ -18,12 +20,14 @@ prost = "0.12.4" prost-types = "0.12.3" ropey = "1.6.0" serde = "1.0.197" -serde_json = "1.0.115" similar = "2.2.1" tokio = "1.36.0" tracing = "0.1.40" tracing-futures = "0.2.5" +# goval-ident features +serde_json = { version = "1.0.116", optional = true } + [lib] name = "services" path = "src/lib.rs" diff --git a/services/src/gcsfiles.rs b/services/src/gcsfiles.rs index 6d8b3a8..7688fb2 100644 --- a/services/src/gcsfiles.rs +++ b/services/src/gcsfiles.rs @@ -12,7 +12,7 @@ use tracing::{debug, warn}; impl traits::Service for GCSFiles { async fn message( &mut self, - _info: &super::types::ChannelInfo, + #[allow(unused)] info: &super::types::ChannelInfo, message: goval::Command, _session: SessionID, ) -> Result> { @@ -64,19 +64,11 @@ impl traits::Service for GCSFiles { let contents = match file.path.as_str() { // TODO: Read this from in the db ".env" => vec![], + #[cfg(feature = "goval-ident")] ".config/goval/info" => { - let val = serde_json::json!({ - "server": "homeval", - "version": env!("CARGO_PKG_VERSION").to_string(), - "license": "AGPL", - "authors": vec!["PotentialStyx <62217716+PotentialStyx@users.noreply.github.com>"], - "repository": "https://github.com/goval-community/homeval", - "description": "", // TODO: do dis - "uptime": 0, // TODO: impl fo realz - "services": super::IMPLEMENTED_SERVICES - }); - - val.to_string().as_bytes().to_vec() + serde_json::to_string(&info.server_info.get_serializable())? + .as_bytes() + .to_vec() } _ => match fs::read(&file.path).await { Err(err) => { diff --git a/services/src/goval_ident.rs b/services/src/goval_ident.rs new file mode 100644 index 0000000..bc90067 --- /dev/null +++ b/services/src/goval_ident.rs @@ -0,0 +1,131 @@ +pub struct GovalIdent {} + +use crate::{server_info::ServerInfoSerialize, ClientInfo, IPCMessage, SessionID}; + +use super::traits; +use anyhow::{format_err, Result}; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use tracing::{error, warn}; + +#[derive(Deserialize)] +enum IdentReqType { + ServerInfo, + + #[serde(untagged)] + Unknown(String), +} + +#[derive(Deserialize)] +struct IdentRequest { + r#type: IdentReqType, + r#ref: String, +} + +#[derive(Serialize)] +#[serde(untagged)] +enum IdentResponseEnum<'a> { + Error(String), + Data(ServerInfoSerialize<'a>), +} + +#[derive(Serialize)] +struct IdentResponse<'a> { + body: IdentResponseEnum<'a>, + r#ref: String, +} + +#[async_trait] +impl traits::Service for GovalIdent { + async fn message( + &mut self, + info: &super::types::ChannelInfo, + message: goval::Command, + session: SessionID, + ) -> Result> { + let body = match message.body.clone() { + None => return Err(format_err!("Expected command body")), + Some(body) => body, + }; + + match body { + goval::command::Body::ChatMessage(msg) => { + let req: IdentRequest = serde_json::from_str(&msg.text)?; + + match req.r#type { + IdentReqType::ServerInfo => { + let msg_text = serde_json::to_string(&IdentResponse { + body: IdentResponseEnum::Data(info.server_info.get_serializable()), + r#ref: req.r#ref, + })?; + + let response_body = goval::command::Body::ChatMessage(goval::ChatMessage { + username: "goval".to_string(), + text: msg_text, + }); + + let response = goval::Command { + body: Some(response_body), + ..Default::default() + }; + + Ok(Some(response)) + } + IdentReqType::Unknown(req_type) => { + error!(req_type, "Unknown `goval-ident` request type"); + + let msg_text = serde_json::to_string(&IdentResponse { + body: IdentResponseEnum::Error(format!( + "Unknown request type {req_type}" + )), + r#ref: req.r#ref, + })?; + + let response_body = goval::command::Body::ChatMessage(goval::ChatMessage { + username: "goval".to_string(), + text: msg_text, + }); + + let response = goval::Command { + body: Some(response_body), + ..Default::default() + }; + + Ok(Some(response)) + } + } + } + goval::command::Body::ChatTyping(typing) => { + warn!( + %session, + username = typing.username, + "Chat#ChatTyping isn't supported by `goval-ident`", + ); + Ok(None) + } + _ => { + warn!(cmd = ?message, "Unknown goval-ident command"); + Ok(None) + } + } + } + + async fn attach( + &mut self, + _info: &super::types::ChannelInfo, + _client: ClientInfo, + _session: SessionID, + _sender: tokio::sync::mpsc::UnboundedSender, + ) -> Result> { + let mut scrollback = goval::Command::default(); + + let _inner = goval::ChatMessage { + username: "goval".to_string(), + text: "{\"type\": \"notification\"}".to_string(), + }; + + scrollback.body = Some(goval::command::Body::ChatMessage(_inner)); + + Ok(Some(scrollback)) + } +} diff --git a/services/src/lib.rs b/services/src/lib.rs index 04e7736..022699f 100644 --- a/services/src/lib.rs +++ b/services/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(extract_if)] +#![feature(extract_if, lazy_cell)] #![warn( clippy::pedantic, clippy::unwrap_used, @@ -36,6 +36,9 @@ mod toolchain; mod traits; mod types; +#[cfg(feature = "goval-ident")] +mod goval_ident; + use anyhow::format_err; use anyhow::Result; use std::collections::HashMap; @@ -58,7 +61,12 @@ impl Channel { dotreplit: Arc>, child_env_vars: Arc>>, sender: tokio::sync::mpsc::UnboundedSender, + + server_info: &'static ServerInfo<'static>, ) -> Result { + #[cfg(feature = "goval-ident")] + let goval_ident = name.as_deref() == Some("goval-ident"); + let info = ChannelInfo { id, name, @@ -68,10 +76,24 @@ impl Channel { sender: sender.clone(), dotreplit, child_env_vars, + + server_info, }; let channel: Box = match service.as_str() { - "chat" => Box::new(chat::Chat::new()), + "chat" => { + #[cfg(feature = "goval-ident")] + { + if goval_ident { + Box::new(goval_ident::GovalIdent {}) + } else { + Box::new(chat::Chat::new()) + } + } + + #[cfg(not(feature = "goval-ident"))] + Box::new(chat::Chat::new()) + } "gcsfiles" => Box::new(gcsfiles::GCSFiles {}), "presence" => Box::new(presence::Presence::new()), "ot" => Box::new(ot::OT::new(sender).await?), diff --git a/services/src/types/channel_info.rs b/services/src/types/channel_info.rs index f49b59f..50e5d9a 100644 --- a/services/src/types/channel_info.rs +++ b/services/src/types/channel_info.rs @@ -6,7 +6,7 @@ use tokio::sync::RwLock; use tracing::error; use crate::config::dotreplit::DotReplit; -use crate::{ChannelID, SessionID}; +use crate::{ChannelID, ServerInfo, SessionID}; use super::client::ClientInfo; use super::messaging::IPCMessage; @@ -27,6 +27,8 @@ pub struct ChannelInfo { pub sender: tokio::sync::mpsc::UnboundedSender, pub dotreplit: Arc>, pub child_env_vars: Arc>>, + + pub server_info: &'static ServerInfo<'static>, } impl ChannelInfo { diff --git a/services/src/types/fs_watcher.rs b/services/src/types/fs_watcher.rs index ff16431..a2ae38a 100644 --- a/services/src/types/fs_watcher.rs +++ b/services/src/types/fs_watcher.rs @@ -3,7 +3,6 @@ use notify_debouncer_full::{ notify::{self, event::ModifyKind, Event, EventKind, RecommendedWatcher, Watcher}, DebounceEventResult, Debouncer, }; -use serde::Serialize; use tracing::error; use anyhow::{format_err, Result}; @@ -20,8 +19,7 @@ use crate::ChannelMessage; // > = LazyLock::new(|| RwLock::new(HashMap::new())); // static MAX_WATCHER: LazyLock> = LazyLock::new(|| Mutex::new(0)); -#[derive(Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone)] pub enum FSEvent { Remove(String), Create(String), diff --git a/services/src/types/messaging.rs b/services/src/types/messaging.rs index 3388f43..3c7878b 100644 --- a/services/src/types/messaging.rs +++ b/services/src/types/messaging.rs @@ -1,14 +1,12 @@ use anyhow::Result; use prost::Message; -use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::Sender; use crate::{SendSessions, SessionID}; use super::client::ClientInfo; -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone)] pub enum ReplspaceMessage { GithubTokenReq(String), // nonce OpenFileReq(String, bool, String), // file, wait for close, nonce diff --git a/services/src/types/mod.rs b/services/src/types/mod.rs index ebc1ce2..f6536fd 100644 --- a/services/src/types/mod.rs +++ b/services/src/types/mod.rs @@ -1,6 +1,9 @@ pub mod channel_info; pub use channel_info::{ChannelInfo, SendSessions}; +pub mod server_info; +pub use server_info::ServerInfo; + pub mod messaging; pub use messaging::{ChannelMessage, IPCMessage, ReplspaceMessage}; diff --git a/services/src/types/server_info.rs b/services/src/types/server_info.rs new file mode 100644 index 0000000..ea781be --- /dev/null +++ b/services/src/types/server_info.rs @@ -0,0 +1,46 @@ +use std::{sync::LazyLock, time::Instant}; + +#[cfg(feature = "goval-ident")] +use serde::Serialize; + +pub struct ServerInfo<'a> { + pub name: &'a str, + pub version: &'a str, + pub license: &'a str, + pub repository: &'a str, + pub description: &'a str, + + pub start_time: &'a LazyLock, + + // TODO: make this &[&str] (is this easily possible?) + pub authors: &'a LazyLock>, +} + +#[cfg(feature = "goval-ident")] +#[derive(Serialize, Debug)] +pub struct ServerInfoSerialize<'a> { + pub server: &'a str, + pub version: &'a str, + pub license: &'a str, + pub authors: &'a [&'a str], + pub repository: &'a str, + pub description: &'a str, + pub uptime: u64, + pub services: &'a [&'a str], +} + +#[cfg(feature = "goval-ident")] +impl<'a> ServerInfo<'a> { + pub fn get_serializable(&self) -> ServerInfoSerialize<'a> { + ServerInfoSerialize { + server: self.name, + version: self.version, + license: self.license, + authors: self.authors, + repository: self.repository, + description: self.description, + uptime: self.start_time.elapsed().as_secs(), + services: crate::IMPLEMENTED_SERVICES, + } + } +} diff --git a/src/goval_server.rs b/src/goval_server.rs index 2b23ddc..2e3496d 100644 --- a/src/goval_server.rs +++ b/src/goval_server.rs @@ -26,8 +26,8 @@ use tokio::sync::mpsc; use tracing::{debug, error, info, trace, warn}; use crate::{ - parse_paseto::parse, ChannelMessage, IPCMessage, CHANNEL_MESSAGES, CHILD_PROCS_ENV_BASE, - DOTREPLIT_CONFIG, LAST_SESSION_USING_CHANNEL, + parse_paseto::parse, server_info::SERVER_INFO, ChannelMessage, IPCMessage, CHANNEL_MESSAGES, + CHILD_PROCS_ENV_BASE, DOTREPLIT_CONFIG, LAST_SESSION_USING_CHANNEL, }; static MAX_SESSION: LazyLock> = LazyLock::new(|| Mutex::new(0)); @@ -281,6 +281,7 @@ async fn open_channel( DOTREPLIT_CONFIG.clone(), CHILD_PROCS_ENV_BASE.clone(), writer, + &SERVER_INFO, ) .await .expect("TODO: Deal with this"); diff --git a/src/main.rs b/src/main.rs index 2842b75..2aa989e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,11 +7,10 @@ )] use anyhow::Result; +use homeval_services::{ChannelID, SessionID}; use std::sync::LazyLock; use std::time::Instant; use std::{collections::HashMap, sync::Arc}; - -use homeval_services::{ChannelID, SessionID}; use tokio::sync::RwLock; use tracing::{debug, info}; @@ -31,6 +30,9 @@ mod replspace_server; #[cfg(feature = "repldb")] mod repldb_server; +mod server_info; +pub use server_info::SERVER_INFO; + pub static START_TIME: LazyLock = LazyLock::new(Instant::now); static CPU_STATS: LazyLock> = LazyLock::new(|| Arc::new(cpu_time::ProcessTime::now())); @@ -68,6 +70,7 @@ async fn main() -> Result<()> { debug!("Initializing lazy statics"); LazyLock::force(&START_TIME); LazyLock::force(&CPU_STATS); + LazyLock::force(SERVER_INFO.authors); debug!("Lazy statics initialized successfully"); diff --git a/src/server_info.rs b/src/server_info.rs new file mode 100644 index 0000000..c954de7 --- /dev/null +++ b/src/server_info.rs @@ -0,0 +1,16 @@ +use std::sync::LazyLock; + +use homeval_services::ServerInfo; + +static SERVER_AUTHORS: LazyLock> = + LazyLock::new(|| env!("CARGO_PKG_AUTHORS").split(':').collect()); + +pub static SERVER_INFO: ServerInfo = ServerInfo { + name: env!("CARGO_PKG_NAME"), + version: env!("CARGO_PKG_VERSION"), + license: env!("CARGO_PKG_LICENSE"), + repository: env!("CARGO_PKG_REPOSITORY"), + description: env!("CARGO_PKG_DESCRIPTION"), + start_time: &crate::START_TIME, + authors: &SERVER_AUTHORS, +};