Switch euph::Room to use euphoxide's Instance
This commit is contained in:
parent
b94dfbdc31
commit
8dd5db5888
5 changed files with 408 additions and 640 deletions
669
src/euph/room.rs
669
src/euph/room.rs
|
|
@ -1,187 +1,143 @@
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::bail;
|
|
||||||
use cookie::{Cookie, CookieJar};
|
|
||||||
use euphoxide::api::packet::ParsedPacket;
|
use euphoxide::api::packet::ParsedPacket;
|
||||||
use euphoxide::api::{
|
use euphoxide::api::{
|
||||||
Auth, AuthOption, Data, Log, Login, Logout, MessageId, Nick, Send, Time, UserId,
|
Auth, AuthOption, Data, Log, Login, Logout, MessageId, Nick, Send, SendEvent, SendReply, Time,
|
||||||
|
UserId,
|
||||||
};
|
};
|
||||||
use euphoxide::conn::{Conn, ConnTx, Joining, State as ConnState};
|
use euphoxide::bot::instance::{Event, Instance, InstanceConfig, Snapshot};
|
||||||
use log::{error, info, warn};
|
use euphoxide::conn::{self, ConnTx};
|
||||||
use parking_lot::Mutex;
|
use log::{debug, error, info, warn};
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::select;
|
||||||
use tokio::{select, task};
|
use tokio::sync::oneshot;
|
||||||
use tokio_tungstenite::tungstenite;
|
|
||||||
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
|
||||||
|
|
||||||
use crate::macros::ok_or_return;
|
use crate::macros::ok_or_return;
|
||||||
use crate::vault::{EuphRoomVault, EuphVault};
|
use crate::vault::EuphRoomVault;
|
||||||
|
|
||||||
const TIMEOUT: Duration = Duration::from_secs(30);
|
|
||||||
const RECONNECT_INTERVAL: Duration = Duration::from_secs(60);
|
|
||||||
const LOG_INTERVAL: Duration = Duration::from_secs(10);
|
const LOG_INTERVAL: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("room stopped")]
|
|
||||||
Stopped,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum EuphRoomEvent {
|
|
||||||
Connected,
|
|
||||||
Disconnected,
|
|
||||||
Packet(Box<ParsedPacket>),
|
|
||||||
Stopped,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Event {
|
pub enum State {
|
||||||
// Events
|
|
||||||
Connected(ConnTx),
|
|
||||||
Disconnected,
|
Disconnected,
|
||||||
Packet(Box<ParsedPacket>),
|
Connecting,
|
||||||
// Commands
|
Connected(ConnTx, conn::State),
|
||||||
State(oneshot::Sender<Option<ConnState>>),
|
Stopped,
|
||||||
RequestLogs,
|
|
||||||
Auth(String),
|
|
||||||
Nick(String),
|
|
||||||
Send(Option<MessageId>, String, oneshot::Sender<MessageId>),
|
|
||||||
Login { email: String, password: String },
|
|
||||||
Logout,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct State {
|
|
||||||
name: String,
|
|
||||||
username: Option<String>,
|
|
||||||
force_username: bool,
|
|
||||||
password: Option<String>,
|
|
||||||
vault: EuphRoomVault,
|
|
||||||
|
|
||||||
conn_tx: Option<ConnTx>,
|
|
||||||
/// `None` before any `snapshot-event`, then either `Some(None)` or
|
|
||||||
/// `Some(Some(id))`.
|
|
||||||
last_msg_id: Option<Option<MessageId>>,
|
|
||||||
requesting_logs: Arc<Mutex<bool>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
async fn run(
|
pub fn conn_tx(&self) -> Option<&ConnTx> {
|
||||||
mut self,
|
if let Self::Connected(conn_tx, _) = self {
|
||||||
canary: oneshot::Receiver<Infallible>,
|
Some(conn_tx)
|
||||||
event_tx: mpsc::UnboundedSender<Event>,
|
|
||||||
mut event_rx: mpsc::UnboundedReceiver<Event>,
|
|
||||||
euph_room_event_tx: mpsc::UnboundedSender<EuphRoomEvent>,
|
|
||||||
ephemeral: bool,
|
|
||||||
) {
|
|
||||||
let vault = self.vault.clone();
|
|
||||||
let name = self.name.clone();
|
|
||||||
let result = if ephemeral {
|
|
||||||
select! {
|
|
||||||
_ = canary => Ok(()),
|
|
||||||
_ = Self::reconnect(&vault, &name, &event_tx) => Ok(()),
|
|
||||||
e = self.handle_events(&mut event_rx, &euph_room_event_tx) => e,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
select! {
|
None
|
||||||
_ = canary => Ok(()),
|
|
||||||
_ = Self::reconnect(&vault, &name, &event_tx) => Ok(()),
|
|
||||||
e = self.handle_events(&mut event_rx, &euph_room_event_tx) => e,
|
|
||||||
_ = Self::regularly_request_logs(&event_tx) => Ok(()),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = result {
|
|
||||||
error!("e&{name}: {}", e);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure that whoever is using this room knows that it's gone.
|
#[derive(Debug, thiserror::Error)]
|
||||||
// Otherwise, the users of the Room may be left in an inconsistent or
|
pub enum Error {
|
||||||
// outdated state, and the UI may not update correctly.
|
#[error("not connected to room")]
|
||||||
let _ = euph_room_event_tx.send(EuphRoomEvent::Stopped);
|
NotConnected,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Room {
|
||||||
|
vault: EuphRoomVault,
|
||||||
|
ephemeral: bool,
|
||||||
|
|
||||||
|
instance: Instance,
|
||||||
|
state: State,
|
||||||
|
|
||||||
|
/// `None` before any `snapshot-event`, then either `Some(None)` or
|
||||||
|
/// `Some(Some(id))`. Reset whenever connection is lost.
|
||||||
|
last_msg_id: Option<Option<MessageId>>,
|
||||||
|
|
||||||
|
/// `Some` while `Self::regularly_request_logs` is running. Set to `None` to
|
||||||
|
/// drop the sender and stop the task.
|
||||||
|
log_request_canary: Option<oneshot::Sender<Infallible>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Room {
|
||||||
|
pub fn new<F>(vault: EuphRoomVault, instance_config: InstanceConfig, on_event: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(Event) + std::marker::Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
// &rl2dev's message history is broken and requesting old messages past
|
||||||
|
// a certain point results in errors. Cove should not keep retrying log
|
||||||
|
// requests when hitting that limit, so &rl2dev is always opened in
|
||||||
|
// ephemeral mode.
|
||||||
|
let ephemeral = vault.vault().vault().ephemeral() || vault.room() == "rl2dev";
|
||||||
|
|
||||||
|
Self {
|
||||||
|
vault,
|
||||||
|
ephemeral,
|
||||||
|
instance: instance_config.build(on_event),
|
||||||
|
state: State::Disconnected,
|
||||||
|
last_msg_id: None,
|
||||||
|
log_request_canary: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reconnect(
|
pub fn stopped(&self) -> bool {
|
||||||
vault: &EuphRoomVault,
|
self.instance.stopped()
|
||||||
name: &str,
|
}
|
||||||
event_tx: &mpsc::UnboundedSender<Event>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
loop {
|
|
||||||
info!("e&{}: connecting", name);
|
|
||||||
let connected = if let Some(mut conn) = Self::connect(vault, name).await? {
|
|
||||||
info!("e&{}: connected", name);
|
|
||||||
event_tx.send(Event::Connected(conn.tx().clone()))?;
|
|
||||||
|
|
||||||
while let Ok(packet) = conn.recv().await {
|
pub fn state(&self) -> &State {
|
||||||
event_tx.send(Event::Packet(Box::new(packet)))?;
|
&self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn conn_tx(&self) -> Result<&ConnTx, Error> {
|
||||||
|
self.state.conn_tx().ok_or(Error::NotConnected)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_event(&mut self, event: Event) {
|
||||||
|
match event {
|
||||||
|
Event::Connecting(_) => {
|
||||||
|
self.state = State::Connecting;
|
||||||
|
|
||||||
|
// Juuust to make sure
|
||||||
|
self.last_msg_id = None;
|
||||||
|
self.log_request_canary = None;
|
||||||
|
}
|
||||||
|
Event::Connected(_, Snapshot { conn_tx, state }) => {
|
||||||
|
if !self.ephemeral {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.log_request_canary = Some(tx);
|
||||||
|
let vault_clone = self.vault.clone();
|
||||||
|
let conn_tx_clone = conn_tx.clone();
|
||||||
|
debug!("{}: spawning log request task", self.instance.config().name);
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
select! {
|
||||||
|
_ = rx => {},
|
||||||
|
_ = Self::regularly_request_logs(vault_clone, conn_tx_clone) => {},
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("e&{}: disconnected", name);
|
self.state = State::Connected(conn_tx, state);
|
||||||
event_tx.send(Event::Disconnected)?;
|
|
||||||
|
|
||||||
true
|
let cookies = &*self.instance.config().server.cookies;
|
||||||
} else {
|
let cookies = cookies.lock().unwrap().clone();
|
||||||
info!("e&{}: could not connect", name);
|
self.vault.vault().set_cookies(cookies);
|
||||||
event_tx.send(Event::Disconnected)?;
|
}
|
||||||
false
|
Event::Packet(_, packet, Snapshot { conn_tx, state }) => {
|
||||||
};
|
self.state = State::Connected(conn_tx, state);
|
||||||
|
self.on_packet(packet);
|
||||||
// Only delay reconnecting if the previous attempt failed. This way,
|
}
|
||||||
// we'll reconnect immediately if we login or logout.
|
Event::Disconnected(_) => {
|
||||||
if !connected {
|
self.state = State::Disconnected;
|
||||||
tokio::time::sleep(RECONNECT_INTERVAL).await;
|
self.last_msg_id = None;
|
||||||
|
self.log_request_canary = None;
|
||||||
|
}
|
||||||
|
Event::Stopped(_) => {
|
||||||
|
// TODO Remove room somewhere if this happens? If it doesn't already happen during stabilization
|
||||||
|
self.state = State::Stopped;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_cookies(vault: &EuphVault) -> String {
|
async fn regularly_request_logs(vault: EuphRoomVault, conn_tx: ConnTx) {
|
||||||
let cookie_jar = vault.cookies().await;
|
|
||||||
let cookies = cookie_jar
|
|
||||||
.iter()
|
|
||||||
.map(|c| format!("{}", c.stripped()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
cookies.join("; ")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_cookies(vault: &EuphVault, set_cookies: &[HeaderValue]) {
|
|
||||||
let mut cookie_jar = CookieJar::new();
|
|
||||||
|
|
||||||
for cookie in set_cookies {
|
|
||||||
if let Ok(cookie) = cookie.to_str() {
|
|
||||||
if let Ok(cookie) = Cookie::from_str(cookie) {
|
|
||||||
cookie_jar.add(cookie)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vault.set_cookies(cookie_jar);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn connect(vault: &EuphRoomVault, name: &str) -> anyhow::Result<Option<Conn>> {
|
|
||||||
// TODO Set user agent?
|
|
||||||
|
|
||||||
let cookies = Self::get_cookies(vault.vault()).await;
|
|
||||||
let cookies = HeaderValue::from_str(&cookies).expect("valid cookies");
|
|
||||||
|
|
||||||
match Conn::connect("euphoria.io", name, true, Some(cookies), TIMEOUT).await {
|
|
||||||
Ok((rx, set_cookies)) => {
|
|
||||||
Self::update_cookies(vault.vault(), &set_cookies);
|
|
||||||
Ok(Some(rx))
|
|
||||||
}
|
|
||||||
Err(tungstenite::Error::Http(resp)) if resp.status().is_client_error() => {
|
|
||||||
bail!("room {name} doesn't exist");
|
|
||||||
}
|
|
||||||
Err(tungstenite::Error::Url(_) | tungstenite::Error::HttpFormat(_)) => {
|
|
||||||
bail!("format error for room {name}");
|
|
||||||
}
|
|
||||||
Err(_) => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn regularly_request_logs(event_tx: &mpsc::UnboundedSender<Event>) {
|
|
||||||
// TODO Make log downloading smarter
|
// TODO Make log downloading smarter
|
||||||
|
|
||||||
// Possible log-related mechanics. Some of these could also run in some
|
// Possible log-related mechanics. Some of these could also run in some
|
||||||
|
|
@ -210,322 +166,130 @@ impl State {
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(LOG_INTERVAL).await;
|
tokio::time::sleep(LOG_INTERVAL).await;
|
||||||
let _ = event_tx.send(Event::RequestLogs);
|
Self::request_logs(&vault, &conn_tx).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_events(
|
async fn request_logs(vault: &EuphRoomVault, conn_tx: &ConnTx) {
|
||||||
&mut self,
|
|
||||||
event_rx: &mut mpsc::UnboundedReceiver<Event>,
|
|
||||||
euph_room_event_tx: &mpsc::UnboundedSender<EuphRoomEvent>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
while let Some(event) = event_rx.recv().await {
|
|
||||||
match event {
|
|
||||||
Event::Connected(conn_tx) => {
|
|
||||||
self.conn_tx = Some(conn_tx);
|
|
||||||
let _ = euph_room_event_tx.send(EuphRoomEvent::Connected);
|
|
||||||
}
|
|
||||||
Event::Disconnected => {
|
|
||||||
self.conn_tx = None;
|
|
||||||
self.last_msg_id = None;
|
|
||||||
let _ = euph_room_event_tx.send(EuphRoomEvent::Disconnected);
|
|
||||||
}
|
|
||||||
Event::Packet(packet) => {
|
|
||||||
self.on_packet(&packet).await?;
|
|
||||||
let _ = euph_room_event_tx.send(EuphRoomEvent::Packet(packet));
|
|
||||||
}
|
|
||||||
Event::State(reply_tx) => self.on_state(reply_tx).await,
|
|
||||||
Event::RequestLogs => self.on_request_logs(),
|
|
||||||
Event::Auth(password) => self.on_auth(password),
|
|
||||||
Event::Nick(name) => self.on_nick(name),
|
|
||||||
Event::Send(parent, content, id_tx) => self.on_send(parent, content, id_tx),
|
|
||||||
Event::Login { email, password } => self.on_login(email, password),
|
|
||||||
Event::Logout => self.on_logout(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn own_user_id(&self) -> Option<UserId> {
|
|
||||||
Some(match self.conn_tx.as_ref()?.state().await.ok()? {
|
|
||||||
ConnState::Joining(Joining { hello, .. }) => hello?.session.id,
|
|
||||||
ConnState::Joined(joined) => joined.session.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn on_packet(&mut self, packet: &ParsedPacket) -> anyhow::Result<()> {
|
|
||||||
let data = ok_or_return!(&packet.content, Ok(()));
|
|
||||||
match data {
|
|
||||||
Data::BounceEvent(_) => {
|
|
||||||
if let Some(password) = &self.password {
|
|
||||||
// Try to authenticate with the configured password, but no
|
|
||||||
// promises if it doesn't work. In particular, we only ever
|
|
||||||
// try this password once.
|
|
||||||
self.on_auth(password.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Data::DisconnectEvent(d) => {
|
|
||||||
warn!("e&{}: disconnected for reason {:?}", self.name, d.reason);
|
|
||||||
}
|
|
||||||
Data::HelloEvent(_) => {}
|
|
||||||
Data::JoinEvent(d) => {
|
|
||||||
info!("e&{}: {:?} joined", self.name, d.0.name);
|
|
||||||
}
|
|
||||||
Data::LoginEvent(_) => {}
|
|
||||||
Data::LogoutEvent(_) => {}
|
|
||||||
Data::NetworkEvent(d) => {
|
|
||||||
info!("e&{}: network event ({})", self.name, d.r#type);
|
|
||||||
}
|
|
||||||
Data::NickEvent(d) => {
|
|
||||||
info!("e&{}: {:?} renamed to {:?}", self.name, d.from, d.to);
|
|
||||||
}
|
|
||||||
Data::EditMessageEvent(_) => {
|
|
||||||
info!("e&{}: a message was edited", self.name);
|
|
||||||
}
|
|
||||||
Data::PartEvent(d) => {
|
|
||||||
info!("e&{}: {:?} left", self.name, d.0.name);
|
|
||||||
}
|
|
||||||
Data::PingEvent(_) => {}
|
|
||||||
Data::PmInitiateEvent(d) => {
|
|
||||||
// TODO Show info popup and automatically join PM room
|
|
||||||
info!(
|
|
||||||
"e&{}: {:?} initiated a pm from &{}",
|
|
||||||
self.name, d.from_nick, d.from_room
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Data::SendEvent(d) => {
|
|
||||||
let own_user_id = self.own_user_id().await;
|
|
||||||
if let Some(last_msg_id) = &mut self.last_msg_id {
|
|
||||||
let id = d.0.id;
|
|
||||||
self.vault
|
|
||||||
.add_msg(Box::new(d.0.clone()), *last_msg_id, own_user_id);
|
|
||||||
*last_msg_id = Some(id);
|
|
||||||
} else {
|
|
||||||
bail!("send event before snapshot event");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Data::SnapshotEvent(d) => {
|
|
||||||
info!("e&{}: successfully joined", self.name);
|
|
||||||
self.vault.join(Time::now());
|
|
||||||
self.last_msg_id = Some(d.log.last().map(|m| m.id));
|
|
||||||
let own_user_id = self.own_user_id().await;
|
|
||||||
self.vault.add_msgs(d.log.clone(), None, own_user_id);
|
|
||||||
|
|
||||||
if let Some(username) = &self.username {
|
|
||||||
if self.force_username || d.nick.is_none() {
|
|
||||||
self.on_nick(username.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Data::LogReply(d) => {
|
|
||||||
let own_user_id = self.own_user_id().await;
|
|
||||||
self.vault.add_msgs(d.log.clone(), d.before, own_user_id);
|
|
||||||
}
|
|
||||||
Data::SendReply(d) => {
|
|
||||||
let own_user_id = self.own_user_id().await;
|
|
||||||
if let Some(last_msg_id) = &mut self.last_msg_id {
|
|
||||||
let id = d.0.id;
|
|
||||||
self.vault
|
|
||||||
.add_msg(Box::new(d.0.clone()), *last_msg_id, own_user_id);
|
|
||||||
*last_msg_id = Some(id);
|
|
||||||
} else {
|
|
||||||
bail!("send reply before snapshot event");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn on_state(&self, reply_tx: oneshot::Sender<Option<ConnState>>) {
|
|
||||||
let state = if let Some(conn_tx) = &self.conn_tx {
|
|
||||||
conn_tx.state().await.ok()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = reply_tx.send(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_request_logs(&self) {
|
|
||||||
if let Some(conn_tx) = &self.conn_tx {
|
|
||||||
// Check whether logs are already being requested
|
|
||||||
let mut guard = self.requesting_logs.lock();
|
|
||||||
if *guard {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
*guard = true;
|
|
||||||
}
|
|
||||||
drop(guard);
|
|
||||||
|
|
||||||
// No logs are being requested and we've reserved our spot, so let's
|
|
||||||
// request some logs!
|
|
||||||
let vault = self.vault.clone();
|
|
||||||
let conn_tx = conn_tx.clone();
|
|
||||||
let requesting_logs = self.requesting_logs.clone();
|
|
||||||
task::spawn(async move {
|
|
||||||
let result = Self::request_logs(vault, conn_tx).await;
|
|
||||||
*requesting_logs.lock() = false;
|
|
||||||
result
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn request_logs(vault: EuphRoomVault, conn_tx: ConnTx) -> anyhow::Result<()> {
|
|
||||||
let before = match vault.last_span().await {
|
let before = match vault.last_span().await {
|
||||||
Some((None, _)) => return Ok(()), // Already at top of room history
|
Some((None, _)) => return, // Already at top of room history
|
||||||
Some((Some(before), _)) => Some(before),
|
Some((Some(before), _)) => Some(before),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
debug!("{}: requesting logs", vault.room());
|
||||||
|
|
||||||
// &rl2dev's message history is broken and requesting old messages past
|
// &rl2dev's message history is broken and requesting old messages past
|
||||||
// a certain point results in errors. By reducing the amount of messages
|
// a certain point results in errors. By reducing the amount of messages
|
||||||
// in each log request, we can get closer to this point. Since &rl2dev
|
// in each log request, we can get closer to this point. Since &rl2dev
|
||||||
// is fairly low in activity, this should be fine.
|
// is fairly low in activity, this should be fine.
|
||||||
let n = if vault.room() == "rl2dev" { 50 } else { 1000 };
|
let n = if vault.room() == "rl2dev" { 50 } else { 1000 };
|
||||||
|
|
||||||
let _ = conn_tx.send(Log { n, before }).await?;
|
let _ = conn_tx.send(Log { n, before }).await;
|
||||||
// The code handling incoming events and replies also handles
|
// The code handling incoming events and replies also handles
|
||||||
// `LogReply`s, so we don't need to do anything special here.
|
// `LogReply`s, so we don't need to do anything special here.
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_auth(&self, password: String) {
|
fn own_user_id(&self) -> Option<UserId> {
|
||||||
if let Some(conn_tx) = &self.conn_tx {
|
if let State::Connected(_, state) = &self.state {
|
||||||
let conn_tx = conn_tx.clone();
|
Some(match state {
|
||||||
task::spawn(async move {
|
conn::State::Joining(joining) => joining.hello.as_ref()?.session.id.clone(),
|
||||||
let _ = conn_tx
|
conn::State::Joined(joined) => joined.session.id.clone(),
|
||||||
.send(Auth {
|
})
|
||||||
r#type: AuthOption::Passcode,
|
} else {
|
||||||
passcode: Some(password),
|
None
|
||||||
})
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_nick(&self, name: String) {
|
fn on_packet(&mut self, packet: ParsedPacket) {
|
||||||
if let Some(conn_tx) = &self.conn_tx {
|
let instance_name = &self.instance.config().name;
|
||||||
let conn_tx = conn_tx.clone();
|
let data = ok_or_return!(&packet.content);
|
||||||
task::spawn(async move {
|
match data {
|
||||||
let _ = conn_tx.send(Nick { name }).await;
|
Data::BounceEvent(_) => {
|
||||||
});
|
if let Some(password) = &self.instance.config().password {
|
||||||
}
|
// Try to authenticate with the configured password, but no
|
||||||
}
|
// promises if it doesn't work. In particular, we only ever
|
||||||
|
// try this password once.
|
||||||
fn on_send(
|
let _ = self.auth(password.clone());
|
||||||
&self,
|
|
||||||
parent: Option<MessageId>,
|
|
||||||
content: String,
|
|
||||||
id_tx: oneshot::Sender<MessageId>,
|
|
||||||
) {
|
|
||||||
if let Some(conn_tx) = &self.conn_tx {
|
|
||||||
let conn_tx = conn_tx.clone();
|
|
||||||
task::spawn(async move {
|
|
||||||
if let Ok(reply) = conn_tx.send(Send { content, parent }).await {
|
|
||||||
let _ = id_tx.send(reply.0.id);
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
Data::DisconnectEvent(d) => {
|
||||||
|
warn!("{instance_name}: disconnected for reason {:?}", d.reason);
|
||||||
|
}
|
||||||
|
Data::HelloEvent(_) => {}
|
||||||
|
Data::JoinEvent(d) => {
|
||||||
|
info!("{instance_name}: {:?} joined", d.0.name);
|
||||||
|
}
|
||||||
|
Data::LoginEvent(_) => {}
|
||||||
|
Data::LogoutEvent(_) => {}
|
||||||
|
Data::NetworkEvent(d) => {
|
||||||
|
info!("{instance_name}: network event ({})", d.r#type);
|
||||||
|
}
|
||||||
|
Data::NickEvent(d) => {
|
||||||
|
info!("{instance_name}: {:?} renamed to {:?}", d.from, d.to);
|
||||||
|
}
|
||||||
|
Data::EditMessageEvent(_) => {
|
||||||
|
info!("{instance_name}: a message was edited");
|
||||||
|
}
|
||||||
|
Data::PartEvent(d) => {
|
||||||
|
info!("{instance_name}: {:?} left", d.0.name);
|
||||||
|
}
|
||||||
|
Data::PingEvent(_) => {}
|
||||||
|
Data::PmInitiateEvent(d) => {
|
||||||
|
// TODO Show info popup and automatically join PM room
|
||||||
|
info!(
|
||||||
|
"{instance_name}: {:?} initiated a pm from &{}",
|
||||||
|
d.from_nick, d.from_room
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Data::SendEvent(SendEvent(msg)) => {
|
||||||
|
let own_user_id = self.own_user_id();
|
||||||
|
if let Some(last_msg_id) = &mut self.last_msg_id {
|
||||||
|
self.vault
|
||||||
|
.add_msg(Box::new(msg.clone()), *last_msg_id, own_user_id);
|
||||||
|
*last_msg_id = Some(msg.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Data::SnapshotEvent(d) => {
|
||||||
|
info!("{instance_name}: successfully joined");
|
||||||
|
self.vault.join(Time::now());
|
||||||
|
self.last_msg_id = Some(d.log.last().map(|m| m.id));
|
||||||
|
self.vault.add_msgs(d.log.clone(), None, self.own_user_id());
|
||||||
|
}
|
||||||
|
Data::LogReply(d) => {
|
||||||
|
self.vault
|
||||||
|
.add_msgs(d.log.clone(), d.before, self.own_user_id());
|
||||||
|
}
|
||||||
|
Data::SendReply(SendReply(msg)) => {
|
||||||
|
let own_user_id = self.own_user_id();
|
||||||
|
if let Some(last_msg_id) = &mut self.last_msg_id {
|
||||||
|
self.vault
|
||||||
|
.add_msg(Box::new(msg.clone()), *last_msg_id, own_user_id);
|
||||||
|
*last_msg_id = Some(msg.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_login(&self, email: String, password: String) {
|
|
||||||
if let Some(conn_tx) = &self.conn_tx {
|
|
||||||
let _ = conn_tx.send(Login {
|
|
||||||
namespace: "email".to_string(),
|
|
||||||
id: email,
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_logout(&self) {
|
|
||||||
if let Some(conn_tx) = &self.conn_tx {
|
|
||||||
let _ = conn_tx.send(Logout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Room {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
canary: oneshot::Sender<Infallible>,
|
|
||||||
event_tx: mpsc::UnboundedSender<Event>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Room {
|
|
||||||
pub fn new(
|
|
||||||
vault: EuphRoomVault,
|
|
||||||
username: Option<String>,
|
|
||||||
force_username: bool,
|
|
||||||
password: Option<String>,
|
|
||||||
) -> (Self, mpsc::UnboundedReceiver<EuphRoomEvent>) {
|
|
||||||
let (canary_tx, canary_rx) = oneshot::channel();
|
|
||||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
|
||||||
let (euph_room_event_tx, euph_room_event_rx) = mpsc::unbounded_channel();
|
|
||||||
|
|
||||||
// &rl2dev's message history is broken and requesting old messages past
|
|
||||||
// a certain point results in errors. Cove should not keep retrying log
|
|
||||||
// requests when hitting that limit, so &rl2dev is always opened in
|
|
||||||
// ephemeral mode.
|
|
||||||
let room_name = vault.room().to_string();
|
|
||||||
let ephemeral = vault.vault().vault().ephemeral() || room_name == "rl2dev";
|
|
||||||
|
|
||||||
let state = State {
|
|
||||||
name: vault.room().to_string(),
|
|
||||||
username,
|
|
||||||
force_username,
|
|
||||||
password,
|
|
||||||
vault,
|
|
||||||
conn_tx: None,
|
|
||||||
last_msg_id: None,
|
|
||||||
requesting_logs: Arc::new(Mutex::new(false)),
|
|
||||||
};
|
|
||||||
|
|
||||||
task::spawn(state.run(
|
|
||||||
canary_rx,
|
|
||||||
event_tx.clone(),
|
|
||||||
event_rx,
|
|
||||||
euph_room_event_tx,
|
|
||||||
ephemeral,
|
|
||||||
));
|
|
||||||
|
|
||||||
let new_room = Self {
|
|
||||||
canary: canary_tx,
|
|
||||||
event_tx,
|
|
||||||
};
|
|
||||||
(new_room, euph_room_event_rx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stopped(&self) -> bool {
|
|
||||||
self.event_tx.is_closed()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn state(&self) -> Result<Option<ConnState>, Error> {
|
|
||||||
let (tx, rx) = oneshot::channel();
|
|
||||||
self.event_tx
|
|
||||||
.send(Event::State(tx))
|
|
||||||
.map_err(|_| Error::Stopped)?;
|
|
||||||
rx.await.map_err(|_| Error::Stopped)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn auth(&self, password: String) -> Result<(), Error> {
|
pub fn auth(&self, password: String) -> Result<(), Error> {
|
||||||
self.event_tx
|
let _ = self.conn_tx()?.send(Auth {
|
||||||
.send(Event::Auth(password))
|
r#type: AuthOption::Passcode,
|
||||||
.map_err(|_| Error::Stopped)
|
passcode: Some(password),
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn log(&self) -> Result<(), Error> {
|
pub fn log(&self) -> Result<(), Error> {
|
||||||
self.event_tx
|
let conn_tx_clone = self.conn_tx()?.clone();
|
||||||
.send(Event::RequestLogs)
|
let vault_clone = self.vault.clone();
|
||||||
.map_err(|_| Error::Stopped)
|
tokio::task::spawn(async move { Self::request_logs(&vault_clone, &conn_tx_clone).await });
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nick(&self, name: String) -> Result<(), Error> {
|
pub fn nick(&self, name: String) -> Result<(), Error> {
|
||||||
self.event_tx
|
let _ = self.conn_tx()?.send(Nick { name });
|
||||||
.send(Event::Nick(name))
|
Ok(())
|
||||||
.map_err(|_| Error::Stopped)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(
|
pub fn send(
|
||||||
|
|
@ -533,22 +297,27 @@ impl Room {
|
||||||
parent: Option<MessageId>,
|
parent: Option<MessageId>,
|
||||||
content: String,
|
content: String,
|
||||||
) -> Result<oneshot::Receiver<MessageId>, Error> {
|
) -> Result<oneshot::Receiver<MessageId>, Error> {
|
||||||
let (id_tx, id_rx) = oneshot::channel();
|
let reply = self.conn_tx()?.send(Send { content, parent });
|
||||||
self.event_tx
|
let (tx, rx) = oneshot::channel();
|
||||||
.send(Event::Send(parent, content, id_tx))
|
tokio::spawn(async move {
|
||||||
.map(|_| id_rx)
|
if let Ok(reply) = reply.await {
|
||||||
.map_err(|_| Error::Stopped)
|
let _ = tx.send(reply.0.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Ok(rx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn login(&self, email: String, password: String) -> Result<(), Error> {
|
pub fn login(&self, email: String, password: String) -> Result<(), Error> {
|
||||||
self.event_tx
|
let _ = self.conn_tx()?.send(Login {
|
||||||
.send(Event::Login { email, password })
|
namespace: "email".to_string(),
|
||||||
.map_err(|_| Error::Stopped)
|
id: email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn logout(&self) -> Result<(), Error> {
|
pub fn logout(&self) -> Result<(), Error> {
|
||||||
self.event_tx
|
let _ = self.conn_tx()?.send(Logout);
|
||||||
.send(Event::Logout)
|
Ok(())
|
||||||
.map_err(|_| Error::Stopped)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
src/ui.rs
15
src/ui.rs
|
|
@ -17,7 +17,6 @@ use tokio::task;
|
||||||
use toss::terminal::Terminal;
|
use toss::terminal::Terminal;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::euph::EuphRoomEvent;
|
|
||||||
use crate::logger::{LogMsg, Logger};
|
use crate::logger::{LogMsg, Logger};
|
||||||
use crate::macros::{ok_or_return, some_or_return};
|
use crate::macros::{ok_or_return, some_or_return};
|
||||||
use crate::vault::Vault;
|
use crate::vault::Vault;
|
||||||
|
|
@ -37,7 +36,7 @@ pub enum UiEvent {
|
||||||
GraphemeWidthsChanged,
|
GraphemeWidthsChanged,
|
||||||
LogChanged,
|
LogChanged,
|
||||||
Term(crossterm::event::Event),
|
Term(crossterm::event::Event),
|
||||||
EuphRoom { name: String, event: EuphRoomEvent },
|
Euph(euphoxide::bot::instance::Event),
|
||||||
}
|
}
|
||||||
|
|
||||||
enum EventHandleResult {
|
enum EventHandleResult {
|
||||||
|
|
@ -94,7 +93,7 @@ impl Ui {
|
||||||
let mut ui = Self {
|
let mut ui = Self {
|
||||||
event_tx: event_tx.clone(),
|
event_tx: event_tx.clone(),
|
||||||
mode: Mode::Main,
|
mode: Mode::Main,
|
||||||
rooms: Rooms::new(config, vault, event_tx.clone()),
|
rooms: Rooms::new(config, vault, event_tx.clone()).await,
|
||||||
log_chat: ChatState::new(logger),
|
log_chat: ChatState::new(logger),
|
||||||
key_bindings_list: None,
|
key_bindings_list: None,
|
||||||
};
|
};
|
||||||
|
|
@ -230,9 +229,8 @@ impl Ui {
|
||||||
self.handle_term_event(terminal, crossterm_lock, event)
|
self.handle_term_event(terminal, crossterm_lock, event)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
UiEvent::EuphRoom { name, event } => {
|
UiEvent::Euph(event) => {
|
||||||
let handled = self.handle_euph_room_event(name, event).await;
|
if self.rooms.handle_euph_event(event) {
|
||||||
if self.mode == Mode::Main && handled {
|
|
||||||
EventHandleResult::Redraw
|
EventHandleResult::Redraw
|
||||||
} else {
|
} else {
|
||||||
EventHandleResult::Continue
|
EventHandleResult::Continue
|
||||||
|
|
@ -311,9 +309,4 @@ impl Ui {
|
||||||
EventHandleResult::Continue
|
EventHandleResult::Continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_euph_room_event(&mut self, name: String, event: EuphRoomEvent) -> bool {
|
|
||||||
let handled = self.rooms.handle_euph_room_event(name, event);
|
|
||||||
handled && self.mode == Mode::Main
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use crossterm::style::{ContentStyle, Stylize};
|
use crossterm::style::{ContentStyle, Stylize};
|
||||||
use euphoxide::api::PersonalAccountView;
|
use euphoxide::api::PersonalAccountView;
|
||||||
use euphoxide::conn::State as ConnState;
|
use euphoxide::conn;
|
||||||
use toss::terminal::Terminal;
|
use toss::terminal::Terminal;
|
||||||
|
|
||||||
use crate::euph::Room;
|
use crate::euph::{self, Room};
|
||||||
use crate::ui::input::{key, InputEvent, KeyBindingsList};
|
use crate::ui::input::{key, InputEvent, KeyBindingsList};
|
||||||
use crate::ui::util;
|
use crate::ui::util;
|
||||||
use crate::ui::widgets::editor::EditorState;
|
use crate::ui::widgets::editor::EditorState;
|
||||||
|
|
@ -14,8 +14,6 @@ use crate::ui::widgets::resize::Resize;
|
||||||
use crate::ui::widgets::text::Text;
|
use crate::ui::widgets::text::Text;
|
||||||
use crate::ui::widgets::BoxedWidget;
|
use crate::ui::widgets::BoxedWidget;
|
||||||
|
|
||||||
use super::room::RoomState;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum Focus {
|
enum Focus {
|
||||||
Email,
|
Email,
|
||||||
|
|
@ -97,8 +95,8 @@ impl AccountUiState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `false` if the account UI should not be displayed any longer.
|
/// Returns `false` if the account UI should not be displayed any longer.
|
||||||
pub fn stabilize(&mut self, state: &RoomState) -> bool {
|
pub fn stabilize(&mut self, state: Option<&euph::State>) -> bool {
|
||||||
if let RoomState::Connected(ConnState::Joined(state)) = state {
|
if let Some(euph::State::Connected(_, conn::State::Joined(state))) = state {
|
||||||
match (&self, &state.account) {
|
match (&self, &state.account) {
|
||||||
(Self::LoggedOut(_), Some(view)) => *self = Self::LoggedIn(LoggedIn(view.clone())),
|
(Self::LoggedOut(_), Some(view)) => *self = Self::LoggedIn(LoggedIn(view.clone())),
|
||||||
(Self::LoggedIn(_), None) => *self = Self::LoggedOut(LoggedOut::new()),
|
(Self::LoggedIn(_), None) => *self = Self::LoggedOut(LoggedOut::new()),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use crossterm::style::{ContentStyle, Stylize};
|
use crossterm::style::{ContentStyle, Stylize};
|
||||||
use euphoxide::api::{Data, Message, MessageId, PacketType, SessionId};
|
use euphoxide::api::{Data, Message, MessageId, PacketType, SessionId};
|
||||||
use euphoxide::conn::{Joined, Joining, SessionInfo, State as ConnState};
|
use euphoxide::bot::instance::{Event, ServerConfig};
|
||||||
|
use euphoxide::conn::{self, Joined, Joining, SessionInfo};
|
||||||
use parking_lot::FairMutex;
|
use parking_lot::FairMutex;
|
||||||
use tokio::sync::oneshot::error::TryRecvError;
|
use tokio::sync::oneshot::error::TryRecvError;
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
@ -11,8 +12,7 @@ use toss::styled::Styled;
|
||||||
use toss::terminal::Terminal;
|
use toss::terminal::Terminal;
|
||||||
|
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::euph::{self, EuphRoomEvent};
|
use crate::euph;
|
||||||
use crate::macros::{ok_or_return, some_or_return};
|
|
||||||
use crate::ui::chat::{ChatState, Reaction};
|
use crate::ui::chat::{ChatState, Reaction};
|
||||||
use crate::ui::input::{key, InputEvent, KeyBindingsList};
|
use crate::ui::input::{key, InputEvent, KeyBindingsList};
|
||||||
use crate::ui::widgets::border::Border;
|
use crate::ui::widgets::border::Border;
|
||||||
|
|
@ -48,26 +48,9 @@ enum State {
|
||||||
InspectSession(SessionInfo),
|
InspectSession(SessionInfo),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
pub enum RoomState {
|
|
||||||
NoRoom,
|
|
||||||
Stopped,
|
|
||||||
Connecting,
|
|
||||||
Connected(ConnState),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RoomState {
|
|
||||||
pub fn connecting_or_connected(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::NoRoom | Self::Stopped => false,
|
|
||||||
Self::Connecting | Self::Connected(_) => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EuphRoom {
|
pub struct EuphRoom {
|
||||||
|
server_config: ServerConfig,
|
||||||
config: config::EuphRoom,
|
config: config::EuphRoom,
|
||||||
|
|
||||||
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
||||||
|
|
||||||
room: Option<euph::Room>,
|
room: Option<euph::Room>,
|
||||||
|
|
@ -84,11 +67,13 @@ pub struct EuphRoom {
|
||||||
|
|
||||||
impl EuphRoom {
|
impl EuphRoom {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
server_config: ServerConfig,
|
||||||
config: config::EuphRoom,
|
config: config::EuphRoom,
|
||||||
vault: EuphRoomVault,
|
vault: EuphRoomVault,
|
||||||
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
server_config,
|
||||||
config,
|
config,
|
||||||
ui_event_tx,
|
ui_event_tx,
|
||||||
room: None,
|
room: None,
|
||||||
|
|
@ -109,36 +94,23 @@ impl EuphRoom {
|
||||||
self.vault().room()
|
self.vault().room()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn shovel_room_events(
|
|
||||||
name: String,
|
|
||||||
mut euph_room_event_rx: mpsc::UnboundedReceiver<EuphRoomEvent>,
|
|
||||||
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
|
||||||
) {
|
|
||||||
loop {
|
|
||||||
let event = some_or_return!(euph_room_event_rx.recv().await);
|
|
||||||
let event = UiEvent::EuphRoom {
|
|
||||||
name: name.clone(),
|
|
||||||
event,
|
|
||||||
};
|
|
||||||
ok_or_return!(ui_event_tx.send(event));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn connect(&mut self) {
|
pub fn connect(&mut self) {
|
||||||
if self.room.is_none() {
|
if self.room.is_none() {
|
||||||
let (room, euph_room_event_rx) = euph::Room::new(
|
let instance_config = self
|
||||||
|
.server_config
|
||||||
|
.clone()
|
||||||
|
.room(self.vault().room().to_string())
|
||||||
|
.username(self.config.username.clone())
|
||||||
|
.force_username(self.config.force_username)
|
||||||
|
.password(self.config.password.clone());
|
||||||
|
|
||||||
|
let tx = self.ui_event_tx.clone();
|
||||||
|
self.room = Some(euph::Room::new(
|
||||||
self.vault().clone(),
|
self.vault().clone(),
|
||||||
self.config.username.clone(),
|
instance_config,
|
||||||
self.config.force_username,
|
move |e| {
|
||||||
self.config.password.clone(),
|
let _ = tx.send(UiEvent::Euph(e));
|
||||||
);
|
},
|
||||||
|
|
||||||
self.room = Some(room);
|
|
||||||
|
|
||||||
tokio::task::spawn(Self::shovel_room_events(
|
|
||||||
self.name().to_string(),
|
|
||||||
euph_room_event_rx,
|
|
||||||
self.ui_event_tx.clone(),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -147,17 +119,16 @@ impl EuphRoom {
|
||||||
self.room = None;
|
self.room = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn state(&self) -> RoomState {
|
pub fn room_state(&self) -> Option<&euph::State> {
|
||||||
match &self.room {
|
if let Some(room) = &self.room {
|
||||||
Some(room) => match room.state().await {
|
Some(room.state())
|
||||||
Ok(Some(state)) => RoomState::Connected(state),
|
} else {
|
||||||
Ok(None) => RoomState::Connecting,
|
None
|
||||||
Err(_) => RoomState::Stopped,
|
|
||||||
},
|
|
||||||
None => RoomState::NoRoom,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO fn room_state_joined(&self) -> Option<&Joined> {}
|
||||||
|
|
||||||
pub fn stopped(&self) -> bool {
|
pub fn stopped(&self) -> bool {
|
||||||
self.room.as_ref().map(|r| r.stopped()).unwrap_or(true)
|
self.room.as_ref().map(|r| r.stopped()).unwrap_or(true)
|
||||||
}
|
}
|
||||||
|
|
@ -190,52 +161,55 @@ impl EuphRoom {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stabilize_focus(&mut self, state: &RoomState) {
|
fn stabilize_focus(&mut self) {
|
||||||
match state {
|
match self.room_state() {
|
||||||
RoomState::Connected(ConnState::Joined(_)) => {}
|
Some(euph::State::Connected(_, conn::State::Joined(_))) => {}
|
||||||
_ => self.focus = Focus::Chat, // There is no nick list to focus on
|
_ => self.focus = Focus::Chat, // There is no nick list to focus on
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stabilize_state(&mut self, state: &RoomState) {
|
fn stabilize_state(&mut self) {
|
||||||
match &mut self.state {
|
let room_state = self.room.as_ref().map(|r| r.state());
|
||||||
State::Auth(_)
|
match (&mut self.state, room_state) {
|
||||||
if !matches!(
|
(
|
||||||
state,
|
State::Auth(_),
|
||||||
RoomState::Connected(ConnState::Joining(Joining {
|
Some(euph::State::Connected(
|
||||||
bounce: Some(_),
|
_,
|
||||||
..
|
conn::State::Joining(Joining {
|
||||||
}))
|
bounce: Some(_), ..
|
||||||
) =>
|
}),
|
||||||
{
|
)),
|
||||||
self.state = State::Normal
|
) => {} // Nothing to see here
|
||||||
}
|
(State::Auth(_), _) => self.state = State::Normal,
|
||||||
State::Nick(_) if !matches!(state, RoomState::Connected(ConnState::Joined(_))) => {
|
|
||||||
self.state = State::Normal
|
(State::Nick(_), Some(euph::State::Connected(_, conn::State::Joined(_)))) => {}
|
||||||
}
|
(State::Nick(_), _) => self.state = State::Normal,
|
||||||
State::Account(account) => {
|
|
||||||
|
(State::Account(account), state) => {
|
||||||
if !account.stabilize(state) {
|
if !account.stabilize(state) {
|
||||||
self.state = State::Normal
|
self.state = State::Normal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stabilize(&mut self, state: &RoomState) {
|
async fn stabilize(&mut self) {
|
||||||
self.stabilize_pseudo_msg().await;
|
self.stabilize_pseudo_msg().await;
|
||||||
self.stabilize_focus(state);
|
self.stabilize_focus();
|
||||||
self.stabilize_state(state);
|
self.stabilize_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn widget(&mut self) -> BoxedWidget {
|
pub async fn widget(&mut self) -> BoxedWidget {
|
||||||
let state = self.state().await;
|
self.stabilize().await;
|
||||||
self.stabilize(&state).await;
|
|
||||||
|
|
||||||
let chat = if let RoomState::Connected(ConnState::Joined(joined)) = &state {
|
let room_state = self.room_state();
|
||||||
self.widget_with_nick_list(&state, joined).await
|
let chat = if let Some(euph::State::Connected(_, conn::State::Joined(joined))) = room_state
|
||||||
|
{
|
||||||
|
self.widget_with_nick_list(room_state, joined).await
|
||||||
} else {
|
} else {
|
||||||
self.widget_without_nick_list(&state).await
|
self.widget_without_nick_list(room_state).await
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut layers = vec![chat];
|
let mut layers = vec![chat];
|
||||||
|
|
@ -257,7 +231,7 @@ impl EuphRoom {
|
||||||
Layer::new(layers).into()
|
Layer::new(layers).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn widget_without_nick_list(&self, state: &RoomState) -> BoxedWidget {
|
async fn widget_without_nick_list(&self, state: Option<&euph::State>) -> BoxedWidget {
|
||||||
VJoin::new(vec![
|
VJoin::new(vec![
|
||||||
Segment::new(Border::new(
|
Segment::new(Border::new(
|
||||||
Padding::new(self.status_widget(state).await).horizontal(1),
|
Padding::new(self.status_widget(state).await).horizontal(1),
|
||||||
|
|
@ -268,7 +242,11 @@ impl EuphRoom {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn widget_with_nick_list(&self, state: &RoomState, joined: &Joined) -> BoxedWidget {
|
async fn widget_with_nick_list(
|
||||||
|
&self,
|
||||||
|
state: Option<&euph::State>,
|
||||||
|
joined: &Joined,
|
||||||
|
) -> BoxedWidget {
|
||||||
HJoin::new(vec![
|
HJoin::new(vec![
|
||||||
Segment::new(VJoin::new(vec![
|
Segment::new(VJoin::new(vec![
|
||||||
Segment::new(Border::new(
|
Segment::new(Border::new(
|
||||||
|
|
@ -293,18 +271,21 @@ impl EuphRoom {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn status_widget(&self, state: &RoomState) -> BoxedWidget {
|
async fn status_widget(&self, state: Option<&euph::State>) -> BoxedWidget {
|
||||||
let room_style = ContentStyle::default().bold().blue();
|
let room_style = ContentStyle::default().bold().blue();
|
||||||
let mut info = Styled::new(format!("&{}", self.name()), room_style);
|
let mut info = Styled::new(format!("&{}", self.name()), room_style);
|
||||||
|
|
||||||
info = match state {
|
info = match state {
|
||||||
RoomState::NoRoom | RoomState::Stopped => info.then_plain(", archive"),
|
None | Some(euph::State::Stopped) => info.then_plain(", archive"),
|
||||||
RoomState::Connecting => info.then_plain(", connecting..."),
|
Some(euph::State::Disconnected) => info.then_plain(", waiting..."),
|
||||||
RoomState::Connected(ConnState::Joining(j)) if j.bounce.is_some() => {
|
Some(euph::State::Connecting) => info.then_plain(", connecting..."),
|
||||||
|
Some(euph::State::Connected(_, conn::State::Joining(j))) if j.bounce.is_some() => {
|
||||||
info.then_plain(", auth required")
|
info.then_plain(", auth required")
|
||||||
}
|
}
|
||||||
RoomState::Connected(ConnState::Joining(_)) => info.then_plain(", joining..."),
|
Some(euph::State::Connected(_, conn::State::Joining(_))) => {
|
||||||
RoomState::Connected(ConnState::Joined(j)) => {
|
info.then_plain(", joining...")
|
||||||
|
}
|
||||||
|
Some(euph::State::Connected(_, conn::State::Joined(j))) => {
|
||||||
let nick = &j.session.name;
|
let nick = &j.session.name;
|
||||||
if nick.is_empty() {
|
if nick.is_empty() {
|
||||||
info.then_plain(", present without nick")
|
info.then_plain(", present without nick")
|
||||||
|
|
@ -326,8 +307,11 @@ impl EuphRoom {
|
||||||
Text::new(info).into()
|
Text::new(info).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_chat_key_bindings(&self, bindings: &mut KeyBindingsList, state: &RoomState) {
|
async fn list_chat_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
||||||
let can_compose = matches!(state, RoomState::Connected(ConnState::Joined(_)));
|
let can_compose = matches!(
|
||||||
|
self.room_state(),
|
||||||
|
Some(euph::State::Connected(_, conn::State::Joined(_)))
|
||||||
|
);
|
||||||
self.chat.list_key_bindings(bindings, can_compose).await;
|
self.chat.list_key_bindings(bindings, can_compose).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -336,9 +320,11 @@ impl EuphRoom {
|
||||||
terminal: &mut Terminal,
|
terminal: &mut Terminal,
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
crossterm_lock: &Arc<FairMutex<()>>,
|
||||||
event: &InputEvent,
|
event: &InputEvent,
|
||||||
state: &RoomState,
|
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let can_compose = matches!(state, RoomState::Connected(ConnState::Joined(_)));
|
let can_compose = matches!(
|
||||||
|
self.room_state(),
|
||||||
|
Some(euph::State::Connected(_, conn::State::Joined(_)))
|
||||||
|
);
|
||||||
|
|
||||||
match self
|
match self
|
||||||
.chat
|
.chat
|
||||||
|
|
@ -368,17 +354,20 @@ impl EuphRoom {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_room_key_bindings(&self, bindings: &mut KeyBindingsList, state: &RoomState) {
|
fn list_room_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
||||||
match state {
|
match self.room_state() {
|
||||||
// Authenticating
|
// Authenticating
|
||||||
RoomState::Connected(ConnState::Joining(Joining {
|
Some(euph::State::Connected(
|
||||||
bounce: Some(_), ..
|
_,
|
||||||
})) => {
|
conn::State::Joining(Joining {
|
||||||
|
bounce: Some(_), ..
|
||||||
|
}),
|
||||||
|
)) => {
|
||||||
bindings.binding("a", "authenticate");
|
bindings.binding("a", "authenticate");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connected
|
// Connected
|
||||||
RoomState::Connected(ConnState::Joined(_)) => {
|
Some(euph::State::Connected(_, conn::State::Joined(_))) => {
|
||||||
bindings.binding("n", "change nick");
|
bindings.binding("n", "change nick");
|
||||||
bindings.binding("m", "download more messages");
|
bindings.binding("m", "download more messages");
|
||||||
bindings.binding("A", "show account ui");
|
bindings.binding("A", "show account ui");
|
||||||
|
|
@ -394,12 +383,15 @@ impl EuphRoom {
|
||||||
bindings.binding("ctrl+p", "open room's plugh.de/present page");
|
bindings.binding("ctrl+p", "open room's plugh.de/present page");
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_room_input_event(&mut self, event: &InputEvent, state: &RoomState) -> bool {
|
async fn handle_room_input_event(&mut self, event: &InputEvent) -> bool {
|
||||||
match state {
|
match self.room_state() {
|
||||||
// Authenticating
|
// Authenticating
|
||||||
RoomState::Connected(ConnState::Joining(Joining {
|
Some(euph::State::Connected(
|
||||||
bounce: Some(_), ..
|
_,
|
||||||
})) => {
|
conn::State::Joining(Joining {
|
||||||
|
bounce: Some(_), ..
|
||||||
|
}),
|
||||||
|
)) => {
|
||||||
if let key!('a') = event {
|
if let key!('a') = event {
|
||||||
self.state = State::Auth(auth::new());
|
self.state = State::Auth(auth::new());
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -407,7 +399,7 @@ impl EuphRoom {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Joined
|
// Joined
|
||||||
RoomState::Connected(ConnState::Joined(joined)) => match event {
|
Some(euph::State::Connected(_, conn::State::Joined(joined))) => match event {
|
||||||
key!('n') | key!('N') => {
|
key!('n') | key!('N') => {
|
||||||
self.state = State::Nick(nick::new(joined.clone()));
|
self.state = State::Nick(nick::new(joined.clone()));
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -463,14 +455,10 @@ impl EuphRoom {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_chat_focus_key_bindings(
|
async fn list_chat_focus_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
||||||
&self,
|
self.list_room_key_bindings(bindings);
|
||||||
bindings: &mut KeyBindingsList,
|
|
||||||
state: &RoomState,
|
|
||||||
) {
|
|
||||||
self.list_room_key_bindings(bindings, state);
|
|
||||||
bindings.empty();
|
bindings.empty();
|
||||||
self.list_chat_key_bindings(bindings, state).await;
|
self.list_chat_key_bindings(bindings).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_chat_focus_input_event(
|
async fn handle_chat_focus_input_event(
|
||||||
|
|
@ -478,18 +466,17 @@ impl EuphRoom {
|
||||||
terminal: &mut Terminal,
|
terminal: &mut Terminal,
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
crossterm_lock: &Arc<FairMutex<()>>,
|
||||||
event: &InputEvent,
|
event: &InputEvent,
|
||||||
state: &RoomState,
|
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// We need to handle chat input first, otherwise the other
|
// We need to handle chat input first, otherwise the other
|
||||||
// key bindings will shadow characters in the editor.
|
// key bindings will shadow characters in the editor.
|
||||||
if self
|
if self
|
||||||
.handle_chat_input_event(terminal, crossterm_lock, event, state)
|
.handle_chat_input_event(terminal, crossterm_lock, event)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.handle_room_input_event(event, state).await {
|
if self.handle_room_input_event(event).await {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -502,17 +489,14 @@ impl EuphRoom {
|
||||||
bindings.binding("i", "inspect session");
|
bindings.binding("i", "inspect session");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_nick_list_focus_input_event(
|
fn handle_nick_list_focus_input_event(&mut self, event: &InputEvent) -> bool {
|
||||||
&mut self,
|
|
||||||
event: &InputEvent,
|
|
||||||
state: &RoomState,
|
|
||||||
) -> bool {
|
|
||||||
if util::handle_list_input_event(&mut self.nick_list, event) {
|
if util::handle_list_input_event(&mut self.nick_list, event) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let key!('i') = event {
|
if let key!('i') = event {
|
||||||
if let RoomState::Connected(ConnState::Joined(joined)) = state {
|
if let Some(euph::State::Connected(_, conn::State::Joined(joined))) = self.room_state()
|
||||||
|
{
|
||||||
if let Some(id) = self.nick_list.cursor() {
|
if let Some(id) = self.nick_list.cursor() {
|
||||||
if id == joined.session.session_id {
|
if id == joined.session.session_id {
|
||||||
self.state =
|
self.state =
|
||||||
|
|
@ -532,15 +516,13 @@ impl EuphRoom {
|
||||||
// Handled in rooms list, not here
|
// Handled in rooms list, not here
|
||||||
bindings.binding("esc", "leave room");
|
bindings.binding("esc", "leave room");
|
||||||
|
|
||||||
let state = self.state().await;
|
|
||||||
|
|
||||||
match self.focus {
|
match self.focus {
|
||||||
Focus::Chat => {
|
Focus::Chat => {
|
||||||
if let RoomState::Connected(ConnState::Joined(_)) = state {
|
if let Some(euph::State::Connected(_, conn::State::Joined(_))) = self.room_state() {
|
||||||
bindings.binding("tab", "focus on nick list");
|
bindings.binding("tab", "focus on nick list");
|
||||||
}
|
}
|
||||||
|
|
||||||
self.list_chat_focus_key_bindings(bindings, &state).await;
|
self.list_chat_focus_key_bindings(bindings).await;
|
||||||
}
|
}
|
||||||
Focus::NickList => {
|
Focus::NickList => {
|
||||||
bindings.binding("tab, esc", "focus on chat");
|
bindings.binding("tab, esc", "focus on chat");
|
||||||
|
|
@ -557,20 +539,18 @@ impl EuphRoom {
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
crossterm_lock: &Arc<FairMutex<()>>,
|
||||||
event: &InputEvent,
|
event: &InputEvent,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let state = self.state().await;
|
|
||||||
|
|
||||||
match self.focus {
|
match self.focus {
|
||||||
Focus::Chat => {
|
Focus::Chat => {
|
||||||
// Needs to be handled first or the tab key may be shadowed
|
// Needs to be handled first or the tab key may be shadowed
|
||||||
// during editing.
|
// during editing.
|
||||||
if self
|
if self
|
||||||
.handle_chat_focus_input_event(terminal, crossterm_lock, event, &state)
|
.handle_chat_focus_input_event(terminal, crossterm_lock, event)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let RoomState::Connected(ConnState::Joined(_)) = state {
|
if let Some(euph::State::Connected(_, conn::State::Joined(_))) = self.room_state() {
|
||||||
if let key!(Tab) = event {
|
if let key!(Tab) = event {
|
||||||
self.focus = Focus::NickList;
|
self.focus = Focus::NickList;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -583,7 +563,7 @@ impl EuphRoom {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.handle_nick_list_focus_input_event(event, &state) {
|
if self.handle_nick_list_focus_input_event(event) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -690,20 +670,31 @@ impl EuphRoom {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_euph_room_event(&mut self, event: EuphRoomEvent) -> bool {
|
pub fn handle_event(&mut self, event: Event) -> bool {
|
||||||
match event {
|
let handled = if self.room.is_some() {
|
||||||
EuphRoomEvent::Connected | EuphRoomEvent::Disconnected | EuphRoomEvent::Stopped => true,
|
if let Event::Packet(_, packet, _) = &event {
|
||||||
EuphRoomEvent::Packet(packet) => match packet.content {
|
match &packet.content {
|
||||||
Ok(data) => self.handle_euph_data(data),
|
Ok(data) => self.handle_euph_data(data),
|
||||||
Err(reason) => self.handle_euph_error(packet.r#type, reason),
|
Err(reason) => self.handle_euph_error(packet.r#type, reason),
|
||||||
},
|
}
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(room) = &mut self.room {
|
||||||
|
room.handle_event(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handled
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_euph_data(&mut self, data: Data) -> bool {
|
fn handle_euph_data(&mut self, data: &Data) -> bool {
|
||||||
// These packets don't result in any noticeable change in the UI.
|
// These packets don't result in any noticeable change in the UI.
|
||||||
#[allow(clippy::match_like_matches_macro)]
|
#[allow(clippy::match_like_matches_macro)]
|
||||||
let handled = match &data {
|
let handled = match data {
|
||||||
Data::PingEvent(_) | Data::PingReply(_) => {
|
Data::PingEvent(_) | Data::PingReply(_) => {
|
||||||
// Pings are displayed nowhere in the room UI.
|
// Pings are displayed nowhere in the room UI.
|
||||||
false
|
false
|
||||||
|
|
@ -720,8 +711,10 @@ impl EuphRoom {
|
||||||
// consistency, some failures are not normal errors but instead
|
// consistency, some failures are not normal errors but instead
|
||||||
// error-free replies that encode their own error.
|
// error-free replies that encode their own error.
|
||||||
let error = match data {
|
let error = match data {
|
||||||
Data::AuthReply(reply) if !reply.success => Some(("authenticate", reply.reason)),
|
Data::AuthReply(reply) if !reply.success => {
|
||||||
Data::LoginReply(reply) if !reply.success => Some(("login", reply.reason)),
|
Some(("authenticate", reply.reason.clone()))
|
||||||
|
}
|
||||||
|
Data::LoginReply(reply) if !reply.success => Some(("login", reply.reason.clone())),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
if let Some((action, reason)) = error {
|
if let Some((action, reason)) = error {
|
||||||
|
|
@ -736,7 +729,7 @@ impl EuphRoom {
|
||||||
handled
|
handled
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_euph_error(&mut self, r#type: PacketType, reason: String) -> bool {
|
fn handle_euph_error(&mut self, r#type: PacketType, reason: &str) -> bool {
|
||||||
let action = match r#type {
|
let action = match r#type {
|
||||||
PacketType::AuthReply => "authenticate",
|
PacketType::AuthReply => "authenticate",
|
||||||
PacketType::NickReply => "set nick",
|
PacketType::NickReply => "set nick",
|
||||||
|
|
@ -762,7 +755,7 @@ impl EuphRoom {
|
||||||
let description = format!("Failed to {action}.");
|
let description = format!("Failed to {action}.");
|
||||||
self.popups.push_front(RoomPopup::Error {
|
self.popups.push_front(RoomPopup::Error {
|
||||||
description,
|
description,
|
||||||
reason,
|
reason: reason.to_string(),
|
||||||
});
|
});
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use crossterm::style::{ContentStyle, Stylize};
|
use crossterm::style::{ContentStyle, Stylize};
|
||||||
use euphoxide::api::SessionType;
|
use euphoxide::api::SessionType;
|
||||||
use euphoxide::conn::{Joined, State as ConnState};
|
use euphoxide::bot::instance::{Event, ServerConfig};
|
||||||
|
use euphoxide::conn::{self, Joined};
|
||||||
use parking_lot::FairMutex;
|
use parking_lot::FairMutex;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use toss::styled::Styled;
|
use toss::styled::Styled;
|
||||||
use toss::terminal::Terminal;
|
use toss::terminal::Terminal;
|
||||||
|
|
||||||
use crate::config::{Config, RoomsSortOrder};
|
use crate::config::{Config, RoomsSortOrder};
|
||||||
use crate::euph::EuphRoomEvent;
|
use crate::euph;
|
||||||
use crate::vault::Vault;
|
use crate::vault::Vault;
|
||||||
|
|
||||||
use super::euph::room::{EuphRoom, RoomState};
|
use super::euph::room::EuphRoom;
|
||||||
use super::input::{key, InputEvent, KeyBindingsList};
|
use super::input::{key, InputEvent, KeyBindingsList};
|
||||||
use super::widgets::editor::EditorState;
|
use super::widgets::editor::EditorState;
|
||||||
use super::widgets::join::{HJoin, Segment, VJoin};
|
use super::widgets::join::{HJoin, Segment, VJoin};
|
||||||
|
|
@ -57,15 +58,20 @@ pub struct Rooms {
|
||||||
|
|
||||||
list: ListState<String>,
|
list: ListState<String>,
|
||||||
order: Order,
|
order: Order,
|
||||||
|
|
||||||
|
euph_server_config: ServerConfig,
|
||||||
euph_rooms: HashMap<String, EuphRoom>,
|
euph_rooms: HashMap<String, EuphRoom>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rooms {
|
impl Rooms {
|
||||||
pub fn new(
|
pub async fn new(
|
||||||
config: &'static Config,
|
config: &'static Config,
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let euph_server_config =
|
||||||
|
ServerConfig::default().cookies(Arc::new(Mutex::new(vault.euph().cookies().await)));
|
||||||
|
|
||||||
let mut result = Self {
|
let mut result = Self {
|
||||||
config,
|
config,
|
||||||
vault,
|
vault,
|
||||||
|
|
@ -73,6 +79,7 @@ impl Rooms {
|
||||||
state: State::ShowList,
|
state: State::ShowList,
|
||||||
list: ListState::new(),
|
list: ListState::new(),
|
||||||
order: Order::from_rooms_sort_order(config.rooms_sort_order),
|
order: Order::from_rooms_sort_order(config.rooms_sort_order),
|
||||||
|
euph_server_config,
|
||||||
euph_rooms: HashMap::new(),
|
euph_rooms: HashMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -90,6 +97,7 @@ impl Rooms {
|
||||||
fn get_or_insert_room(&mut self, name: String) -> &mut EuphRoom {
|
fn get_or_insert_room(&mut self, name: String) -> &mut EuphRoom {
|
||||||
self.euph_rooms.entry(name.clone()).or_insert_with(|| {
|
self.euph_rooms.entry(name.clone()).or_insert_with(|| {
|
||||||
EuphRoom::new(
|
EuphRoom::new(
|
||||||
|
self.euph_server_config.clone(),
|
||||||
self.config.euph_room(&name),
|
self.config.euph_room(&name),
|
||||||
self.vault.euph().room(name),
|
self.vault.euph().room(name),
|
||||||
self.ui_event_tx.clone(),
|
self.ui_event_tx.clone(),
|
||||||
|
|
@ -236,15 +244,18 @@ impl Rooms {
|
||||||
result.join(" ")
|
result.join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_room_state(state: RoomState) -> Option<String> {
|
fn format_room_state(state: Option<&euph::State>) -> Option<String> {
|
||||||
match state {
|
match state {
|
||||||
RoomState::NoRoom | RoomState::Stopped => None,
|
None | Some(euph::State::Stopped) => None,
|
||||||
RoomState::Connecting => Some("connecting".to_string()),
|
Some(euph::State::Disconnected) => Some("waiting".to_string()),
|
||||||
RoomState::Connected(ConnState::Joining(j)) if j.bounce.is_some() => {
|
Some(euph::State::Connecting) => Some("connecting".to_string()),
|
||||||
Some("auth required".to_string())
|
Some(euph::State::Connected(_, connected)) => match connected {
|
||||||
}
|
conn::State::Joining(joining) if joining.bounce.is_some() => {
|
||||||
RoomState::Connected(ConnState::Joining(_)) => Some("joining".to_string()),
|
Some("auth required".to_string())
|
||||||
RoomState::Connected(ConnState::Joined(joined)) => Some(Self::format_pbln(&joined)),
|
}
|
||||||
|
conn::State::Joining(_) => Some("joining".to_string()),
|
||||||
|
conn::State::Joined(joined) => Some(Self::format_pbln(joined)),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,7 +267,7 @@ impl Rooms {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_room_info(state: RoomState, unseen: usize) -> Styled {
|
fn format_room_info(state: Option<&euph::State>, unseen: usize) -> Styled {
|
||||||
let unseen_style = ContentStyle::default().bold().green();
|
let unseen_style = ContentStyle::default().bold().green();
|
||||||
|
|
||||||
let state = Self::format_room_state(state);
|
let state = Self::format_room_state(state);
|
||||||
|
|
@ -276,12 +287,16 @@ impl Rooms {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sort_rooms(&self, rooms: &mut [(&String, RoomState, usize)]) {
|
fn sort_rooms(&self, rooms: &mut [(&String, Option<&euph::State>, usize)]) {
|
||||||
match self.order {
|
match self.order {
|
||||||
Order::Alphabet => rooms.sort_unstable_by_key(|(n, _, _)| *n),
|
Order::Alphabet => rooms.sort_unstable_by_key(|(n, _, _)| *n),
|
||||||
Order::Importance => {
|
Order::Importance => rooms.sort_unstable_by_key(|(n, s, u)| {
|
||||||
rooms.sort_unstable_by_key(|(n, s, u)| (!s.connecting_or_connected(), *u == 0, *n))
|
let connecting_or_connected = matches!(
|
||||||
}
|
s,
|
||||||
|
Some(euph::State::Connecting | euph::State::Connected(_, _))
|
||||||
|
);
|
||||||
|
(!connecting_or_connected, *u == 0, *n)
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,7 +310,7 @@ impl Rooms {
|
||||||
|
|
||||||
let mut rooms = vec![];
|
let mut rooms = vec![];
|
||||||
for (name, room) in &self.euph_rooms {
|
for (name, room) in &self.euph_rooms {
|
||||||
let state = room.state().await;
|
let state = room.room_state();
|
||||||
let unseen = room.unseen_msgs_count().await;
|
let unseen = room.unseen_msgs_count().await;
|
||||||
rooms.push((name, state, unseen));
|
rooms.push((name, state, unseen));
|
||||||
}
|
}
|
||||||
|
|
@ -536,15 +551,15 @@ impl Rooms {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_euph_room_event(&mut self, name: String, event: EuphRoomEvent) -> bool {
|
pub fn handle_euph_event(&mut self, event: Event) -> bool {
|
||||||
let room_visible = if let State::ShowRoom(n) = &self.state {
|
let instance_name = event.config().name.clone();
|
||||||
*n == name
|
let room = self.get_or_insert_room(instance_name.clone());
|
||||||
} else {
|
let handled = room.handle_event(event);
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
let room = self.get_or_insert_room(name);
|
let room_visible = match &self.state {
|
||||||
let handled = room.handle_euph_room_event(event);
|
State::ShowRoom(name) => *name == instance_name,
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
handled && room_visible
|
handled && room_visible
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue