Commit f64f0f97 authored by Paul Woolcock's avatar Paul Woolcock

Merge branch 'blocking' into 'master'

Blocking

See merge request !2
parents d2920859 4d7c3727
Pipeline #59805097 passed with stage
in 3 minutes and 19 seconds
This diff is collapsed.
......@@ -7,7 +7,7 @@ edition = "2018"
[dependencies]
tui = "0.5.1"
termion = "1.5.1"
elefren = { version = "0.19", features = ["toml"] }
elefren = "0.19"
unicode-width = "0.1.5"
soup = "0.3"
reqwest = "0.9"
......@@ -17,3 +17,7 @@ actix = "0.8"
snafu = "0.2"
chrono = "0.4"
derive-new = "0.5"
toml = "0.5.0"
serde = "1.0.90"
serde_derive = "1.0.90"
futures = "0.1.26"
......@@ -2,6 +2,7 @@ use actix::prelude::*;
use derive_new::new;
use elefren::status_builder::StatusBuilder;
use snafu::{ResultExt, Snafu};
use futures::future::{self, Future};
use crate::{client, logger, repo};
......@@ -21,16 +22,16 @@ impl Error {
}
#[derive(new)]
pub struct Channel(Addr<Action>);
pub struct Channel(Addr<Action>, logger::Channel);
impl Channel {
pub fn input_async(&self, s: String) -> Result<(), Error> {
self.0.do_send(Wrapper(s));
Ok(())
pub fn input(&self, s: String) -> Box<Future<Item=Result<(), Error>, Error=Error>> {
self.1.log(format!("got input '{}'", &s));
Box::new(self.0.send(Wrapper(s)).map_err(|e| Error::MailboxError { source: e }))
}
}
#[derive(Debug)]
struct Wrapper(String);
pub struct Wrapper(pub String);
impl From<Wrapper> for String {
fn from(w: Wrapper) -> String {
w.0
......@@ -53,15 +54,29 @@ impl Actor for Action {
fn started(&mut self, _: &mut Context<Self>) {
self.logger.log("Action actor starting");
}
fn stopping(&mut self, _: &mut Context<Self>) -> actix::Running {
self.logger.log("Action actor stopping");
actix::Running::Stop
}
fn stopped(&mut self, _: &mut Context<Self>) {
self.logger.log("Action actor stopped");
}
}
impl Handler<Wrapper> for Action {
type Result = Result<(), Error>;
type Result = Box<Future<Item=(), Error=Error>>;
fn handle(&mut self, msg: Wrapper, _ctx: &mut Context<Self>) -> Self::Result {
let request = parse_post(msg.into(), &self.logger)?;
self.logger.log(format!("handling message {:?}", msg));
let request = match parse_post(msg.into(), &self.logger) {
Ok(r) => r,
Err(e) => return Box::new(future::err(e)),
};
self.logger.log(format!("parsed input is {:?}", request));
match request {
/*
message::Request::Post(post) => {
let vis = self.repo.get_vis().context(RepoError)?;
let status = StatusBuilder {
......@@ -89,11 +104,15 @@ impl Handler<Wrapper> for Action {
self.client.favourite(&post.id).context(ClientError)?;
}
*/
message::Request::ShowMore(ref id) => {
self.repo.show_more(id).context(RepoError)?;
Box::new(self.repo.show_more(id).map_err(|e| Error::RepoError { source: e }))
}
/*
message::Request::Reply(id, content) => {
self.logger.log("About to contact repo");
let entry = self.repo.get(&id).context(RepoError)?;
self.logger.log(format!("got entry {:?}", entry));
let post = entry.status();
let content = format!("@{} {}", &post.account.acct, content);
let status = StatusBuilder {
......@@ -105,15 +124,32 @@ impl Handler<Wrapper> for Action {
};
self.client.new_status(status).context(ClientError)?;
}
*/
message::Request::Block(user) => {
Box::new(
self.client
.block(user)
.map_err(|e| Error::ClientError { source: e })
.map(|_| ())
)
}
message::Request::SetVis(vis) => {
self.repo.set_vis(vis).context(RepoError)?;
Box::new(
self.repo
.set_vis(vis)
.map_err(|e| Error::RepoError { source: e })
.map(|_| ())
)
}
message::Request::Quit => {
self.logger.log("Got message to quit");
System::current().stop();
Box::new(future::ok(()))
}
_ => {
Box::new(future::ok(()))
}
}
Ok(())
}
}
......@@ -129,6 +165,7 @@ mod message {
PostWithCw(String, String),
ShowMore(String),
Reply(String, String),
Block(String),
Quit,
}
......@@ -180,6 +217,7 @@ fn parse_post(s: String, _: &logger::Channel) -> Result<message::Request, Error>
_ => return Err(Error::custom("did not recognize privacy setting")),
})
}
"/block" | "/b" => Ok(message::Request::Block(content.to_string())),
"/exit" | "/quit" | "/q" => Ok(message::Request::Quit),
_ => return Err(Error::custom("didn't understand command")),
}
......
......@@ -11,9 +11,16 @@ use crate::logger;
pub enum Error {
ElefrenError { source: elefren::Error },
MailboxError { source: actix::MailboxError },
Custom { message: String },
WrongResponse,
}
impl Error {
fn custom<I: Into<String>>(msg: I) -> Error {
Error::Custom { message: msg.into() }
}
}
pub struct Client {
client: Mastodon,
logger: logger::Channel,
......@@ -57,10 +64,48 @@ impl Handler<message::Request> for Client {
let tl = self.client.get_home_timeline().context(ElefrenError)?;
message::Response::Timeline(tl.items_iter().take(num).collect::<Vec<_>>())
}
message::Request::Block(ref user) => {
self.logger.log(format!("blocking user {}", user));
let search = self.client.search_v2(user, true).context(ElefrenError)?;
let account = if search.accounts.is_empty() {
self.logger.log("Couldn't fitnd account");
return Err(Error::custom(format!("Could not find account '{}'", user)));
} else if search.accounts.len() > 1 {
self.logger.log("Found more than one account");
match find_account(&user, &search.accounts, &self.logger) {
Some(account_idx) => &search.accounts[account_idx],
None => return Err(Error::custom(format!("Could not find account '{}'", user))),
}
} else {
self.logger.log(format!("found single account {:?}", &search.accounts[0]));
&search.accounts[0]
};
if let Err(e) = self.client.block(&account.id) {
self.logger.log(format!("couldn't block account, error was {:?}", e));
}
message::Response::Empty
}
})
}
}
fn find_account(user: &str, accounts: &[elefren::entities::account::Account], logger: &logger::Channel) -> Option<usize> {
let mut parts = user.splitn(2, '@');
let username = parts.next()?;
for (i, account) in accounts.iter().enumerate() {
// special case: the account to be blocked is on the same server as the logged-in user
if account.acct == username {
logger.log(format!("returning user {:?}", &account.acct));
return Some(i);
}
if account.acct == user {
logger.log(format!("returning user {:?}", &account.acct));
return Some(i);
}
}
None
}
type Stream = EventReader<BufReader<ReqResponse>>;
mod message {
......@@ -72,6 +117,7 @@ mod message {
Post(elefren::status_builder::StatusBuilder),
Fav(String),
HomeTimeline(usize),
Block(String),
Stream,
}
......@@ -136,4 +182,18 @@ impl Channel {
Err(Error::WrongResponse)
}
}
pub fn block(&self, user: String) -> impl Future<Item=(), Error=Error> {
self.0.send(message::Request::Block(user))
.map_err(|e: actix::MailboxError| Error::MailboxError { source: e })
.map(|_| ())
/*
let resp = fut.wait().context(MailboxError)?;
if let message::Response::Empty = resp? {
Ok(())
} else {
Err(Error::WrongResponse)
}
*/
}
}
use snafu::{ResultExt, Snafu};
use toml;
use elefren::Data;
use serde_derive::{Serialize, Deserialize};
use std::{
fs::{File, OpenOptions},
io::{self, Read, BufWriter, Write},
path::Path,
};
#[derive(Debug, Snafu)]
pub enum Error {
IoError { source: io::Error },
TomlDeError { source: toml::de::Error },
TomlSerError { source: toml::ser::Error },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub client: Data,
}
impl Config {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Config, Error> {
let path = path.as_ref();
let mut file = File::open(path).context(IoError)?;
let mut buff = Vec::new();
file.read_to_end(&mut buff).context(IoError)?;
Ok(toml::from_slice(&buff).context(TomlDeError)?)
}
pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
let file = OpenOptions::new().create(true).write(true).open(path.as_ref()).context(IoError)?;
let mut file = BufWriter::new(file);
let buff = toml::to_vec(self).context(TomlSerError)?;
file.write(&buff).context(IoError)?;
Ok(())
}
}
......@@ -22,6 +22,7 @@ const LOG_FILE: &'static str = "fedichat.log";
mod action;
mod client;
mod config;
mod entry;
mod event;
mod logger;
......@@ -36,6 +37,7 @@ enum Error {
LoggerError { source: crate::logger::Error },
ClientError { source: crate::client::Error },
UiError { source: crate::ui::Error },
ConfigError { source: crate::config::Error },
}
type Result<T> = ::std::result::Result<T, crate::Error>;
......@@ -52,11 +54,15 @@ struct Opts {
fn register(config: &str, server: &str) -> Result<elefren::Data> {
use elefren::{
helpers::{cli, toml},
helpers::cli,
prelude::*,
};
Ok(match toml::from_file(config) {
Ok(t) => t,
if server.trim().is_empty() {
eprintln!("a server must be provided in order to authenticate");
::std::process::exit(1);
}
Ok(match config::Config::from_file(config) {
Ok(t) => t.client,
Err(..) => {
let hostname = if server.starts_with("https://") {
server.to_string()
......@@ -65,11 +71,14 @@ fn register(config: &str, server: &str) -> Result<elefren::Data> {
};
let reg = Registration::new(hostname)
.client_name("fedichat")
.scopes(Scopes::read_all() | Scopes::write_all())
.scopes(Scopes::read_all() | Scopes::write_all() | Scopes::follow())
.build()
.context(ElefrenError)?;
let m = cli::authenticate(reg).context(ElefrenError)?;
toml::to_file(&m, config).context(ElefrenError)?;
let config_data = config::Config {
client: m.data.clone(),
};
config_data.to_file(config).context(ConfigError)?;
m.data
}
})
......@@ -109,12 +118,14 @@ fn main() -> Result<()> {
.start();
let (ui_repo, ui_logger) = (repo.clone(), logger.clone());
let input_logger = logger.clone();
let action_logger = logger::Channel::with_ctx(logger.clone(), "ActionChannel".to_string());
let ui_action = action.clone();
let ui = Supervisor::start_in_arbiter(&ui_arbiter, move |_| {
Ui::new(
terminal,
ui_repo,
logger::Channel::with_ctx(ui_logger, "UI".to_string()),
action::Channel::new(action.clone()),
action::Channel::new(ui_action, action_logger),
)
});
Supervisor::start_in_arbiter(&input_arbiter, move |_| {
......
......@@ -114,6 +114,7 @@ impl Handler<message::Request> for Repo {
}
message::Request::GetVis => message::Response::Vis(self.vis.clone()),
message::Request::SetVis(vis) => {
self.logger.log("got SetVis message");
self.vis = vis;
message::Response::Vis(self.vis.clone())
}
......@@ -166,14 +167,13 @@ impl Channel {
}
}
pub fn show_more(&self, id: &str) -> Result<(), Error> {
pub fn show_more(&self, id: &str) -> Box<Future<Item=(), Error=Error>> {
let fut = self.0.send(message::Request::ShowMore(id.to_string()));
let resp = fut.wait().context(MailboxError)?;
if let message::Response::Empty = resp? {
Ok(())
} else {
Err(Error::WrongResponse)
}
let fut = fut.map_err(|e| Error::MailboxError { source: e});
let fut = fut.map(|e: Result<message::Response, Error>| { // TODO needs to be handled
()
});
Box::new(fut)
}
pub fn get_vis(&self) -> Result<Visibility, Error> {
......@@ -186,14 +186,17 @@ impl Channel {
}
}
pub fn set_vis(&self, visibility: Visibility) -> Result<Visibility, Error> {
let fut = self.0.send(message::Request::SetVis(visibility));
let resp = fut.wait().context(MailboxError)?;
if let message::Response::Vis(visibility) = resp? {
Ok(visibility)
} else {
Err(Error::WrongResponse)
}
pub fn set_vis(&self, visibility: Visibility) -> Box<Future<Item=Visibility, Error=Error>> {
Box::new(self.0.send(message::Request::SetVis(visibility))
.map_err(|e: actix::MailboxError| Error::MailboxError{ source: e })
.and_then(|res: Result<message::Response, Error>| {
match res {
Ok(message::Response::Vis(vis)) => Ok(vis),
Ok(..) => Err(Error::WrongResponse),
Err(e) => Err(e),
}
})
)
}
}
......
use actix::prelude::*;
use actix::utils::IntervalFunc;
use snafu::{ResultExt, Snafu};
use snafu::Snafu;
use std::{io::{self, Write}, time::Duration};
use termion::cursor::Goto;
......@@ -14,6 +14,7 @@ use tui::{
Terminal,
};
use unicode_width::UnicodeWidthStr;
use futures::future::{self, Future};
use crate::{action, logger, repo};
......@@ -110,23 +111,30 @@ impl actix::Supervised for Ui {
}
impl Handler<message::Request> for Ui {
type Result = Result<(), Error>;
type Result = Box<Future<Item=(), Error=Error>>;
fn handle(&mut self, msg: message::Request, _: &mut Context<Self>) -> Self::Result {
Ok(match msg {
match msg {
message::Request::Push(c) => {
self.input.push(c);
Box::new(future::ok(()))
},
message::Request::Pop => {
self.input.pop();
Box::new(future::ok(()))
},
message::Request::Drain => {
self.logger.log("Got drain message");
let input = self.input.drain(..).collect::<String>();
self.logger.log("sending input to Action actor");
self.action.input_async(input).context(ActionError)?;
self.logger.log(format!("sending input '{}' to Action actor", &input));
Box::new(
self.action.input(input)
.map(|_| ()) // TODO the value coming in here is Result<(), action::Error>,
// and the error really needs to be handled
.map_err(|e| Error::ActionError { source: e })
)
}
})
}
}
}
......@@ -144,83 +152,3 @@ pub mod message {
type Result = Result<(), Error>;
}
}
/*
use crossbeam::Sender;
use std::{
default::Default,
io::{self, Write},
};
use crate::error::Result;
use crate::event::{Event, Events};
use crate::{logger, repo};
#[derive(Debug, Default, Clone, PartialEq)]
struct ChatState {
input: String,
}
pub fn run(sender: Sender<String>, repo: repo::Channel, logger: logger::Channel) -> Result<!> {
logger.log("Initializing UI");
let stdout = io::stdout().into_raw_mode()?;
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut chat_state = ChatState::default();
let events = Events::new();
loop {
terminal.draw(|mut f| {
let vis = if let Ok(vis) = repo.get_vis() {
vis
} else {
return;
};
let title = format!("TOOT - {:?}", vis);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
.split(f.size());
Paragraph::new([Text::raw(&chat_state.input)].iter())
.style(Style::default().fg(Color::Yellow))
.block(Block::default().borders(Borders::ALL).title(&title))
.render(&mut f, chunks[0]);
let list_height = chunks[1].height as usize;
let statuses = if let Ok(statuses) = repo.get_latest(list_height) {
statuses
} else {
return;
};
let statuses = statuses
.iter()
.map(|status| Text::raw(format!("{}", status)));
List::new(statuses)
.block(Block::default().borders(Borders::ALL).title("FEED"))
.render(&mut f, chunks[1]);
})?;
write!(
terminal.backend_mut(),
"{}",
Goto(4 + chat_state.input.width() as u16, 4)
)?;
match events.next()? {
Event::Input(input) => match input {
Key::Char('\n') => {
let input = chat_state.input.drain(..).collect::<String>();
sender.send(input)?;
}
Key::Char(c) => {
chat_state.input.push(c);
}
Key::Backspace => {
chat_state.input.pop();
}
_ => {}
},
_ => {}
}
}
}
*/
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment