}
}
-#[derive(Serialize, Deserialize, Debug)]
+#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct AuthConfig {
pub account_id: Option<String>,
pub username: Option<String>,
use std::fs::File;
use std::io::{IoSlice, Read, Write};
-use super::auth::{AuthConfig, AuthError};
-use super::config::ConfigLocation;
+use super::auth::AuthConfig;
use super::posting::Post;
use super::types::*;
}
pub struct Client {
- auth: AuthConfig,
+ pub auth: AuthConfig,
client: reqwest::blocking::Client,
accounts: HashMap<String, Account>,
statuses: HashMap<String, Status>,
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum ClientError {
- Auth(AuthError), // message
InternalError(String), // message
UrlParseError(String, String), // url, message
UrlError(String, String), // url, message
impl super::TopLevelErrorCandidate for ClientError {}
-impl From<AuthError> for ClientError {
- fn from(err: AuthError) -> Self {
- ClientError::Auth(err)
- }
-}
-
impl From<reqwest::Error> for ClientError {
fn from(err: reqwest::Error) -> Self {
match err.url() {
f: &mut std::fmt::Formatter<'_>,
) -> Result<(), std::fmt::Error> {
match self {
- ClientError::Auth(ref autherr) => {
- write!(f, "unable to read authentication: {}", autherr)
- }
ClientError::InternalError(ref msg) => {
write!(f, "internal failure: {}", msg)
}
Ok((rsp, log))
}
+const REDIRECT_MAGIC_STRING: &str = "urn:ietf:wg:oauth:2.0:oob";
+
+pub enum AppTokenType<'a> {
+ ClientCredentials,
+ AuthorizationCode(&'a str),
+}
+
impl Client {
- pub fn new(cfgloc: &ConfigLocation) -> Result<Self, ClientError> {
+ pub fn new(auth: AuthConfig) -> Result<Self, ClientError> {
Ok(Client {
- auth: AuthConfig::load(cfgloc)?,
+ auth,
client: reqwest_client()?,
accounts: HashMap::new(),
statuses: HashMap::new(),
self.polls.insert(poll.id.to_string(), poll.clone());
}
- pub fn account_by_id(&mut self, id: &str) -> Result<Account, ClientError> {
- // If we're fetching our own account, we do it via the
+ fn account_by_id_internal(
+ &mut self,
+ id: Option<&str>,
+ ) -> Result<Account, ClientError> {
+ // If we're fetching our own account, or we don't know our
+ // account id yet because we're still logging in, we use the
// verify_credentials request, which gets the extra
// information. This also means we must repeat the request if
// we've already received a copy of our own account details
// via some other API and it doesn't contain the extra
// information.
- if let Some(ac) = self.accounts.get(id) {
+ if let Some(ac) = id.and_then(|id| self.accounts.get(id)) {
if Some(&ac.id) != self.auth.account_id.as_ref()
|| ac.source.is_some()
{
}
}
- let req = if Some(id) == self.auth.account_id.as_deref() {
+ let req = if id == self.auth.account_id.as_deref() {
Req::get(&format!("api/v1/accounts/verify_credentials"))
} else {
- Req::get(&format!("api/v1/accounts/{id}"))
+ Req::get(&format!("api/v1/accounts/{}", id.unwrap()))
};
let (url, rsp) = self.api_request(req)?;
let rspstatus = rsp.status();
}
}
}?;
- if ac.id != id {
+ if id.is_some_and(|id| ac.id != id) {
return Err(ClientError::UrlError(
url,
format!("request returned wrong account id {}", &ac.id),
Ok(ac)
}
+ pub fn account_by_id(&mut self, id: &str) -> Result<Account, ClientError> {
+ self.account_by_id_internal(Some(id))
+ }
+
+ pub fn verify_account_credentials(
+ &mut self,
+ ) -> Result<Account, ClientError> {
+ self.account_by_id_internal(None)
+ }
+
pub fn poll_by_id(&mut self, id: &str) -> Result<Poll, ClientError> {
if let Some(st) = self.polls.get(id) {
return Ok(st.clone());
self.cache_account(&ac);
Ok(())
}
+
+ pub fn register_client(&mut self) -> Result<Application, ClientError> {
+ let req = Req::post("/api/v1/apps")
+ .param("redirect_uris", REDIRECT_MAGIC_STRING)
+ .param("client_name", "Mastodonochrome")
+ .param("scopes", "read write push")
+ .param("website", "https://www.chiark.greenend.org.uk/~sgtatham/mastodonochrome/");
+ let (url, rsp) = self.api_request(req)?;
+ let rspstatus = rsp.status();
+ if !rspstatus.is_success() {
+ Err(ClientError::UrlError(url, rspstatus.to_string()))
+ } else {
+ let app: Application = match serde_json::from_str(&rsp.text()?) {
+ Ok(app) => Ok(app),
+ Err(e) => Err(ClientError::UrlError(url, e.to_string())),
+ }?;
+ Ok(app)
+ }
+ }
+
+ pub fn get_app_token(
+ &mut self,
+ app: &Application,
+ toktype: AppTokenType,
+ ) -> Result<Token, ClientError> {
+ let client_id = match &app.client_id {
+ Some(id) => Ok(id),
+ None => Err(ClientError::InternalError(
+ "registering application did not return a client id"
+ .to_owned(),
+ )),
+ }?;
+ let client_secret = match &app.client_secret {
+ Some(id) => Ok(id),
+ None => Err(ClientError::InternalError(
+ "registering application did not return a client secret"
+ .to_owned(),
+ )),
+ }?;
+
+ let req = Req::post("/oauth/token")
+ .param("redirect_uri", REDIRECT_MAGIC_STRING)
+ .param("client_id", client_id)
+ .param("client_secret", client_secret);
+ let req = match toktype {
+ AppTokenType::ClientCredentials => {
+ req.param("grant_type", "client_credentials")
+ }
+ AppTokenType::AuthorizationCode(code) => req
+ .param("grant_type", "authorization_code")
+ .param("code", code)
+ .param("scope", "read write push"),
+ };
+ let (url, rsp) = self.api_request(req)?;
+ let rspstatus = rsp.status();
+ if !rspstatus.is_success() {
+ Err(ClientError::UrlError(url, rspstatus.to_string()))
+ } else {
+ let tok: Token = match serde_json::from_str(&rsp.text()?) {
+ Ok(tok) => Ok(tok),
+ Err(e) => Err(ClientError::UrlError(url, e.to_string())),
+ }?;
+ Ok(tok)
+ }
+ }
+
+ pub fn verify_app_credentials(
+ &mut self,
+ ) -> Result<Application, ClientError> {
+ let req = Req::get("api/v1/apps/verify_credentials");
+ let (url, rsp) = self.api_request(req)?;
+ let rspstatus = rsp.status();
+ if !rspstatus.is_success() {
+ Err(ClientError::UrlError(url, rspstatus.to_string()))
+ } else {
+ let app: Application = match serde_json::from_str(&rsp.text()?) {
+ Ok(app) => Ok(app),
+ Err(e) => Err(ClientError::UrlError(url, e.to_string())),
+ }?;
+ Ok(app)
+ }
+ }
+
+ pub fn get_auth_url(
+ &self,
+ app: &Application,
+ ) -> Result<String, ClientError> {
+ let client_id = match &app.client_id {
+ Some(id) => Ok(id),
+ None => Err(ClientError::InternalError(
+ "registering application did not return a client id"
+ .to_owned(),
+ )),
+ }?;
+
+ let (_urlstr, url) = Req::get("/oauth/authorize")
+ .param("redirect_uri", REDIRECT_MAGIC_STRING)
+ .param("client_id", client_id)
+ .param("scope", "read write push")
+ .param("response_type", "code")
+ .url(self.auth.instance_url.as_deref().expect("should have set up an instance URL before calling get_auth_url"))?;
+ Ok(url.to_string())
+ }
}
use std::io::Write;
use super::auth::{AuthConfig, AuthError};
-use super::client::{
- execute_and_log_request, reqwest_client, ClientError, Req,
-};
+use super::client::{Client, ClientError, AppTokenType};
use super::config::ConfigLocation;
-use super::types::{Account, Application, Instance, Token};
+use super::types::{Account, Application, Instance};
use super::TopLevelError;
-struct Login {
- instance_url: String,
- client: reqwest::blocking::Client,
- logfile: Option<File>,
-}
-
-enum AppTokenType<'a> {
- ClientCredentials,
- AuthorizationCode(&'a str),
-}
-use AppTokenType::*;
-
-const REDIRECT_MAGIC_STRING: &str = "urn:ietf:wg:oauth:2.0:oob";
-
-impl Login {
- fn new(
- instance_url: &str,
- logfile: Option<File>,
- ) -> Result<Self, ClientError> {
- Ok(Login {
- instance_url: instance_url.to_owned(),
- client: reqwest_client()?,
- logfile,
- })
- }
-
- fn execute_request(
- &mut self,
- req: reqwest::blocking::RequestBuilder,
- ) -> Result<reqwest::blocking::Response, ClientError> {
- let (rsp, log) = execute_and_log_request(&self.client, req.build()?)?;
- log.write_to(&mut self.logfile);
- Ok(rsp)
- }
-
- fn register_client(&mut self) -> Result<Application, ClientError> {
- let (url, req) = Req::post("/api/v1/apps")
- .param("redirect_uris", REDIRECT_MAGIC_STRING)
- .param("client_name", "Mastodonochrome")
- .param("scopes", "read write push")
- .param("website", "https://www.chiark.greenend.org.uk/~sgtatham/mastodonochrome/")
- .build(&self.instance_url, &self.client, None)?;
- let rsp = self.execute_request(req)?;
- let rspstatus = rsp.status();
- if !rspstatus.is_success() {
- Err(ClientError::UrlError(url, rspstatus.to_string()))
- } else {
- let app: Application = match serde_json::from_str(&rsp.text()?) {
- Ok(app) => Ok(app),
- Err(e) => Err(ClientError::UrlError(url, e.to_string())),
- }?;
- Ok(app)
- }
- }
-
- fn get_token(
- &mut self,
- app: &Application,
- toktype: AppTokenType,
- ) -> Result<Token, ClientError> {
- let client_id = match &app.client_id {
- Some(id) => Ok(id),
- None => Err(ClientError::InternalError(
- "registering application did not return a client id"
- .to_owned(),
- )),
- }?;
- let client_secret = match &app.client_secret {
- Some(id) => Ok(id),
- None => Err(ClientError::InternalError(
- "registering application did not return a client secret"
- .to_owned(),
- )),
- }?;
-
- let req = Req::post("/oauth/token")
- .param("redirect_uri", REDIRECT_MAGIC_STRING)
- .param("client_id", client_id)
- .param("client_secret", client_secret);
- let req = match toktype {
- ClientCredentials => req.param("grant_type", "client_credentials"),
- AuthorizationCode(code) => req
- .param("grant_type", "authorization_code")
- .param("code", code)
- .param("scope", "read write push"),
- };
- let (url, req) = req.build(&self.instance_url, &self.client, None)?;
- let rsp = self.execute_request(req)?;
- let rspstatus = rsp.status();
- if !rspstatus.is_success() {
- Err(ClientError::UrlError(url, rspstatus.to_string()))
- } else {
- let tok: Token = match serde_json::from_str(&rsp.text()?) {
- Ok(tok) => Ok(tok),
- Err(e) => Err(ClientError::UrlError(url, e.to_string())),
- }?;
- Ok(tok)
- }
- }
-
- fn get<T: for<'a> serde::Deserialize<'a>>(
- &mut self,
- path: &str,
- token: &str,
- ) -> Result<T, ClientError> {
- let (url, req) = Req::get(path).build(
- &self.instance_url,
- &self.client,
- Some(token),
- )?;
- let rsp = self.execute_request(req)?;
- let rspstatus = rsp.status();
- if !rspstatus.is_success() {
- Err(ClientError::UrlError(url, rspstatus.to_string()))
- } else {
- let tok: T = match serde_json::from_str(&rsp.text()?) {
- Ok(tok) => Ok(tok),
- Err(e) => Err(ClientError::UrlError(url, e.to_string())),
- }?;
- Ok(tok)
- }
- }
-
- fn get_auth_url(&self, app: &Application) -> Result<String, ClientError> {
- let client_id = match &app.client_id {
- Some(id) => Ok(id),
- None => Err(ClientError::InternalError(
- "registering application did not return a client id"
- .to_owned(),
- )),
- }?;
-
- let (_urlstr, url) = Req::get("/oauth/authorize")
- .param("redirect_uri", REDIRECT_MAGIC_STRING)
- .param("client_id", client_id)
- .param("scope", "read write push")
- .param("response_type", "code")
- .url(&self.instance_url)?;
- Ok(url.to_string())
- }
-}
-
pub fn login(
cfgloc: &ConfigLocation,
instance_url: &str,
}?;
let instance_url = url.as_str().trim_end_matches('/');
- let mut login = Login::new(instance_url, logfile)?;
+ let mut client = Client::new(AuthConfig::default())?;
+ client.set_logfile(logfile);
+ client.set_writable(true);
+ client.auth.instance_url = Some(instance_url.to_owned());
// Register the client and get its details
- let app = login.register_client()?;
- let app_token = login.get_token(&app, ClientCredentials)?;
- let _app: Application = login
- .get("/api/v1/apps/verify_credentials", &app_token.access_token)?;
+ let app = client.register_client()?;
+ let app_token = client.get_app_token(&app, AppTokenType::ClientCredentials)?;
+ client.auth.user_token = Some(app_token.access_token);
+ let _app: Application = client.verify_app_credentials()?;
// Get the URL the user will have to visit
- let url = login.get_auth_url(&app)?;
+ let url = client.get_auth_url(&app)?;
// Print it
println!("Log in to the website {instance_url}/");
let code = code.trim_end();
// Use that code to get the final user access token
- let user_token = login.get_token(&app, AuthorizationCode(code))?;
- let account: Account = login.get(
- "/api/v1/accounts/verify_credentials",
- &user_token.access_token,
- )?;
- let instance: Instance =
- login.get("/api/v2/instance", &user_token.access_token)?;
+ let token = client.get_app_token(&app, AppTokenType::AuthorizationCode(code))?;
+ client.auth.user_token = Some(token.access_token.clone());
+ let account: Account = client.verify_account_credentials()?;
+ let instance: Instance = client.instance()?;
println!();
println!(
instance_domain: Some(instance.domain),
client_id: Some(app.client_id.unwrap()),
client_secret: Some(app.client_secret.unwrap()),
- user_token: Some(user_token.access_token),
+ user_token: Some(token.access_token),
};
let mut json = serde_json::to_string_pretty(&auth).unwrap();
use unicode_width::UnicodeWidthStr;
use super::activity_stack::*;
+use super::auth::{AuthConfig, AuthError};
use super::client::{
Client, ClientError, FeedExtend, FeedId, StreamId, StreamUpdate,
};
}
}
}
+impl From<AuthError> for TuiError {
+ fn from(err: AuthError) -> Self {
+ TuiError {
+ message: format!("unable to read authentication: {}", err)
+ }
+ }
+}
impl From<std::sync::mpsc::RecvError> for TuiError {
fn from(err: std::sync::mpsc::RecvError) -> Self {
TuiError {
}
});
- let mut client = Client::new(cfgloc)?;
+ let auth = AuthConfig::load(cfgloc)?;
+ let mut client = Client::new(auth)?;
client.set_writable(!readonly);
client.set_logfile(logfile);