feat: implemented commit command

This commit is contained in:
Himadri Bhattacharjee
2025-04-19 09:51:18 +05:30
parent 829b264ba9
commit e5e4aeaff1
3 changed files with 196 additions and 23 deletions

View File

@@ -17,8 +17,9 @@ interface or by editing it externally.
- [x] Authfile path
- [x] Listening port number
- [x] `/add` command to add new keys
Run it as `echo "ssh-ed25519 somekey" | ssh chatroom`
- [x] `/rename` command
- [ ] `/commit` command to commit in-memory changes to Authfile
- [x] `/commit` command to commit in-memory changes to Authfile
- [ ] `#mention` tags
### Authfile

View File

@@ -70,7 +70,7 @@ pub struct AuthFile {
pub key_pool: HashSet<KeyData>,
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Entity {
name: String,
role: Role,

View File

@@ -49,6 +49,8 @@ pub enum Error {
Authfile(#[from] authfile::Error),
#[error("failed to resize frame as requested by client {id}")]
FrameResize { source: std::io::Error, id: usize },
#[error("failed to parse command {0:#?}")]
CommandParse(String),
}
pub struct Client {
@@ -204,7 +206,10 @@ impl AppServer {
let id = self.id;
tokio::spawn(async move {
let mut clients = clients.write().await;
let client = clients.get_mut(&id).unwrap();
let Some(client) = clients.get_mut(&id) else {
log::warn!("failed to get handle on the current client with id: {}", id);
return;
};
let res = client.terminal.draw(|f| {
let buf = f.buffer_mut();
@@ -310,6 +315,7 @@ impl Handler for AppServer {
data: &[u8],
_session: &mut Session,
) -> Result<(), Self::Error> {
log::info!("you sent me: {data:#?}");
match data {
// Sending Ctrl+C ends the session and disconnects the client
[3] => return Err(russh::Error::Disconnect.into()),
@@ -326,8 +332,7 @@ impl Handler for AppServer {
};
let text = current_client.textarea.lines().to_vec().join("\n");
// HACK: Clear the textarea on send.
// Select all, delete.
// HACK: Clear the textarea on send. Select all, delete.
current_client.textarea.select_all();
current_client
.textarea
@@ -337,8 +342,15 @@ impl Handler for AppServer {
text
};
let name = self.entity().await.read().await.name().to_string();
// TODO: handle commands
let Some(command) = Command::parse(&text) else {
let maybe_command = match Command::parse(&text) {
Ok(c) => c,
Err(e) => {
log::error!("{e:?}");
return Ok(());
}
};
let Some(command) = maybe_command else {
let message = format!("[{name}]: {text}");
self.app.write().await.history.push(message);
self.render().await;
@@ -347,15 +359,27 @@ impl Handler for AppServer {
match command {
Command::Add(entity) => {
if self.entity().await.read().await.role() != authfile::Role::Admin {
// TODO: tell em "you're not an admin lol kekw"
return Ok(());
}
log::info!("attempting to add {:#?}", entity);
let mut keychain = self.keychain.write().await;
let mut key_data_pool = self.key_data_pool.write().await;
let mut key_data_to_user = self.key_data_to_user.write().await;
let key_data = entity.key_data();
keychain.push(new_atomic(entity));
key_data_pool.insert(key_data);
let entity = new_atomic(entity);
keychain.push(entity.clone());
key_data_pool.insert(key_data.clone());
key_data_to_user.insert(key_data, entity);
}
Command::Rename { from, to } => {
if self.entity().await.read().await.role() != authfile::Role::Admin {
// TODO: tell em "you're not an admin lol kekw"
return Ok(());
}
let from = from
.split_once(':')
.map(|(sanitized, _ignore_role)| sanitized.to_string())
@@ -378,6 +402,8 @@ impl Handler for AppServer {
let ent = ent.read().await.clone();
let kd_2_id = self.key_data_to_id.read().await;
// TODO: remove stray ids from key_data_to_id
// and id_to_user
let ids = kd_2_id.get(&ent.key_data()).unwrap();
for id in ids {
let mut clients = self.clients.write().await;
@@ -385,7 +411,7 @@ impl Handler for AppServer {
log::warn!(
"failed to get handle on client with id: {id}, considering them disconnected"
);
return Ok(());
continue;
};
let block = Block::bordered()
@@ -395,6 +421,33 @@ impl Handler for AppServer {
}
}
}
Command::Commit => {
if self.entity().await.read().await.role() != authfile::Role::Admin {
// TODO: tell em "you're not an admin lol kekw"
return Ok(());
}
let keychain = self.keychain.read().await;
let mut pubkeys = vec![];
for entity in keychain.iter() {
let ent_str = entity.read().await.to_pubkey().to_string();
pubkeys.push(ent_str);
}
let pubkeys = pubkeys.join("\n");
let mut tmpfile = self.args.authfile.clone();
tmpfile.push('~');
if let Err(e) = std::fs::write(&tmpfile, pubkeys) {
log::error!(
"failed to create temporary file to commit in-memory authorized keys: {e:#?}"
);
return Ok(());
};
if let Err(e) = std::fs::rename(tmpfile, &self.args.authfile) {
log::error!(
"failed to move temporary file to original authfile: {e:#?}: do we have write permissions to it?"
);
return Ok(());
};
}
}
// re-render
self.render().await;
@@ -415,6 +468,118 @@ impl Handler for AppServer {
self.render_textarea().await;
}
data if data.ends_with(&[10]) => {
let name = self.entity().await.read().await.name().to_string();
let text = std::str::from_utf8(&data[..data.len() - 1]).unwrap();
let maybe_command = match Command::parse(&text) {
Ok(c) => c,
Err(e) => {
log::error!("{e:?}");
return Ok(());
}
};
let Some(command) = maybe_command else {
let message = format!("[{name}]: {text}");
self.app.write().await.history.push(message);
self.render().await;
return Ok(());
};
match command {
Command::Add(entity) => {
if self.entity().await.read().await.role() != authfile::Role::Admin {
// TODO: tell em "you're not an admin lol kekw"
return Ok(());
}
log::info!("attempting to add {:#?}", entity);
let mut keychain = self.keychain.write().await;
let mut key_data_pool = self.key_data_pool.write().await;
let mut key_data_to_user = self.key_data_to_user.write().await;
let key_data = entity.key_data();
let entity = new_atomic(entity);
keychain.push(entity.clone());
key_data_pool.insert(key_data.clone());
key_data_to_user.insert(key_data, entity);
}
Command::Rename { from, to } => {
if self.entity().await.read().await.role() != authfile::Role::Admin {
// TODO: tell em "you're not an admin lol kekw"
return Ok(());
}
let from = from
.split_once(':')
.map(|(sanitized, _ignore_role)| sanitized.to_string())
.unwrap_or(from);
let to = to
.split_once(':')
.map(|(sanitized, _ignore_role)| sanitized.to_string())
.unwrap_or(to);
log::info!("renaming {:?} to {:?}", from, to);
let kc = self.keychain.read().await;
for ent in kc.iter() {
if ent.read().await.name() != from {
continue;
}
ent.write().await.set_name(&to);
let ent = ent.read().await.clone();
let kd_2_id = self.key_data_to_id.read().await;
// TODO: remove stray ids from key_data_to_id
// and id_to_user
let ids = kd_2_id.get(&ent.key_data()).unwrap();
for id in ids {
let mut clients = self.clients.write().await;
let Some(client) = clients.get_mut(id) else {
log::warn!(
"failed to get handle on client with id: {id}, considering them disconnected"
);
continue;
};
let block = Block::bordered()
.border_type(BorderType::Rounded)
.title(ent.title());
client.textarea.set_block(block);
}
}
}
Command::Commit => {
if self.entity().await.read().await.role() != authfile::Role::Admin {
// TODO: tell em "you're not an admin lol kekw"
return Ok(());
}
let keychain = self.keychain.read().await;
let mut pubkeys = vec![];
for entity in keychain.iter() {
let ent_str = entity.read().await.to_pubkey().to_string();
pubkeys.push(ent_str);
}
let pubkeys = pubkeys.join("\n");
let mut tmpfile = self.args.authfile.clone();
tmpfile.push('~');
if let Err(e) = std::fs::write(&tmpfile, pubkeys) {
log::error!(
"failed to create temporary file to commit in-memory authorized keys: {e:#?}"
);
return Ok(());
};
if let Err(e) = std::fs::rename(tmpfile, &self.args.authfile) {
log::error!(
"failed to move temporary file to original authfile: {e:#?}: do we have write permissions to it?"
);
return Ok(());
};
}
}
}
data if !data.is_empty() => {
let mut iterator = data.iter().skip(1).map(|d| Ok(*d));
match ratatui::termion::event::parse_event(data[0], &mut iterator) {
@@ -423,13 +588,15 @@ impl Handler for AppServer {
self.check_role_and_reload().await?;
}
Ok(keycode) => {
self.clients
.write()
.await
.get_mut(&self.id)
.unwrap()
.textarea
.input(keycode);
let mut clients = self.clients.write().await;
let Some(client) = clients.get_mut(&self.id) else {
log::warn!(
"failed to get handle on the current client with id: {}",
self.id
);
return Ok(());
};
client.textarea.input(keycode);
}
Err(e) => {
log::warn!("failed to parse keyboard input data: {:?}: {e}", data);
@@ -535,13 +702,18 @@ impl Drop for AppServer {
pub enum Command {
Add(Entity),
Rename { from: String, to: String },
Commit,
}
impl Command {
fn parse(text: &str) -> Option<Self> {
fn parse(text: &str) -> Result<Option<Self>, Error> {
let split = text.split_once(char::is_whitespace);
Some(match split {
Some(("/add", payload)) => Self::Add(payload.try_into().unwrap()),
if text == "/commit" {
return Ok(Some(Self::Commit));
}
Ok(Some(match split {
Some(("/add", payload)) => Self::Add(payload.try_into()?),
Some(("/rename", payload)) => {
let split_payload: Vec<&str> = payload.split_whitespace().collect();
match split_payload.as_slice() {
@@ -549,11 +721,11 @@ impl Command {
to: to.to_string(),
from: from.to_string(),
},
_ => return None, // TODO: return an error message
_ => return Err(Error::CommandParse(text.to_string())),
}
}
_ => return None,
})
_ => return Ok(None),
}))
}
}