feat: implemented commit command
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -70,7 +70,7 @@ pub struct AuthFile {
|
||||
pub key_pool: HashSet<KeyData>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Entity {
|
||||
name: String,
|
||||
role: Role,
|
||||
|
||||
214
src/main.rs
214
src/main.rs
@@ -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),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user