├── src ├── lib.rs ├── archive.rs ├── api │ └── oauth.rs ├── api.rs └── main.rs ├── .gitignore ├── README.md ├── Cargo.toml └── Cargo.lock /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod archive; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.zip 3 | /twitter-*/ 4 | /cache.json 5 | /ornithology.html 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a silly little tool that you pass your [Twitter archive] to, and 2 | it generates a HTML page with some notable tweets from your history and 3 | some of your more interesting followers. 4 | 5 | Install with 6 | 7 | ```console 8 | $ cargo install ornithology-cli 9 | ``` 10 | 11 | Run with 12 | 13 | ```console 14 | $ ornithology path/to/twitter/archive 15 | ``` 16 | 17 | It should open your browser once to authenticate with Twitter, and once 18 | to render the page with interesting tweets. See `--help` for more stuff 19 | you can do. Here's [an example] for [my Twitter account]. 20 | 21 | [Twitter archive]: https://help.twitter.com/en/managing-your-account/how-to-download-your-twitter-archive 22 | [an example]: https://jon.thesquareplanet.com/share/ornithology.html 23 | [my Twitter account]: https://twitter.com/jonhoo 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ornithology-cli" 3 | version = "0.1.1" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | repository = "https://github.com/jonhoo/ornithology" 7 | categories = ["command-line-utilities"] 8 | description = "A tool that parses your Twitter archive and highlights interesting data from it." 9 | 10 | [dependencies] 11 | anyhow = "1" 12 | axum = { version = "0.5", features = ["http2"] } 13 | clap = { version = "3", features = ["derive"] } 14 | futures-util = { version = "0.3" } 15 | indicatif = "0.16" 16 | oauth2 = "4.2" 17 | open = "3" 18 | rand = "0.8" 19 | reqwest = { version = "0.11", features = ["json"] } 20 | serde_json = "1" 21 | serde_urlencoded = "0.7" 22 | serde = { version = "1", features = ["derive"] } 23 | time = { version = "0.3", features = ["serde", "serde-well-known"] } 24 | tokio = { version = "1", features = ["full"] } 25 | tower = { version = "0.4", features = ["limit", "retry"] } 26 | url = "2" 27 | zip = "0.6" 28 | 29 | [[bin]] 30 | path = "src/main.rs" 31 | name = "ornithology" 32 | -------------------------------------------------------------------------------- /src/archive.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | pub fn parse( 4 | archive: &mut zip::ZipArchive, 5 | datafile: &str, 6 | mut map: F, 7 | ) -> anyhow::Result 8 | where 9 | R: std::io::Read + std::io::Seek, 10 | T: serde::de::DeserializeOwned, 11 | F: FnMut(T) -> Option, 12 | C: FromIterator, 13 | { 14 | use anyhow::Context; 15 | let mut file = archive 16 | .by_name(datafile) 17 | .with_context(|| format!("pick {} from archive", datafile))?; 18 | let mut contents = String::new(); 19 | std::io::Read::read_to_string(&mut file, &mut contents) 20 | .with_context(|| format!("read {}", datafile))?; 21 | 22 | let data_start = contents 23 | .find('[') 24 | .with_context(|| format!("find [ indicating start of data in {}", datafile))?; 25 | let data = &contents[data_start..]; 26 | let mut data = data.as_bytes(); 27 | let deser = serde_json_array_iter::iter_json_array(&mut data); 28 | Ok(deser 29 | .filter_map(|v| v.map(&mut map).transpose()) 30 | .collect::>() 31 | .with_context(|| format!("parse {}", datafile))?) 32 | } 33 | 34 | #[derive(Debug, Deserialize)] 35 | pub enum Follower { 36 | #[serde(rename = "follower")] 37 | One { 38 | #[serde(rename = "accountId", deserialize_with = "u64_from_str")] 39 | id: u64, 40 | }, 41 | } 42 | 43 | #[derive(Debug, Deserialize)] 44 | pub enum Tweet { 45 | #[serde(rename = "tweet")] 46 | One { 47 | #[serde(rename = "id", deserialize_with = "u64_from_str")] 48 | id: u64, 49 | #[serde(rename = "full_text")] 50 | text: String, 51 | }, 52 | } 53 | 54 | fn u64_from_str<'de, D>(deserializer: D) -> Result 55 | where 56 | D: serde::de::Deserializer<'de>, 57 | { 58 | let s = String::deserialize(deserializer)?; 59 | s.parse().map_err(serde::de::Error::custom) 60 | } 61 | 62 | // https://github.com/serde-rs/json/issues/404 63 | mod serde_json_array_iter { 64 | use serde::de::DeserializeOwned; 65 | use serde_json::{self, Deserializer}; 66 | use std::io::{self, Read}; 67 | 68 | fn read_skipping_ws(mut reader: impl Read) -> io::Result { 69 | loop { 70 | let mut byte = 0u8; 71 | reader.read_exact(std::slice::from_mut(&mut byte))?; 72 | if !byte.is_ascii_whitespace() { 73 | return Ok(byte); 74 | } 75 | } 76 | } 77 | 78 | fn invalid_data(msg: &str) -> io::Error { 79 | io::Error::new(io::ErrorKind::InvalidData, msg) 80 | } 81 | 82 | fn deserialize_single(reader: R) -> io::Result { 83 | let next_obj = Deserializer::from_reader(reader).into_iter::().next(); 84 | match next_obj { 85 | Some(result) => result.map_err(Into::into), 86 | None => Err(invalid_data("premature EOF")), 87 | } 88 | } 89 | 90 | fn yield_next_obj( 91 | mut reader: R, 92 | at_start: &mut bool, 93 | ) -> io::Result> { 94 | if !*at_start { 95 | *at_start = true; 96 | if read_skipping_ws(&mut reader)? == b'[' { 97 | // read the next char to see if the array is empty 98 | let peek = read_skipping_ws(&mut reader)?; 99 | if peek == b']' { 100 | Ok(None) 101 | } else { 102 | deserialize_single(io::Cursor::new([peek]).chain(reader)).map(Some) 103 | } 104 | } else { 105 | Err(invalid_data("`[` not found")) 106 | } 107 | } else { 108 | match read_skipping_ws(&mut reader)? { 109 | b',' => deserialize_single(reader).map(Some), 110 | b']' => Ok(None), 111 | _ => Err(invalid_data("`,` or `]` not found")), 112 | } 113 | } 114 | } 115 | 116 | pub fn iter_json_array( 117 | mut reader: R, 118 | ) -> impl Iterator> { 119 | let mut at_start = false; 120 | std::iter::from_fn(move || yield_next_obj(&mut reader, &mut at_start).transpose()) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/api/oauth.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use axum::{extract::Query, routing::get, Router}; 3 | use oauth2::{HttpRequest, HttpResponse, RedirectUrl}; 4 | use reqwest::StatusCode; 5 | use serde::{Deserialize, Serialize}; 6 | use std::fmt; 7 | use std::{ 8 | net::SocketAddr, 9 | sync::{Arc, Mutex}, 10 | }; 11 | 12 | /// The data passed along with an OAuth authorization redirect. 13 | #[derive(Debug, Deserialize, Serialize)] 14 | #[serde(untagged)] 15 | pub(super) enum Redirect { 16 | Authorized { code: String, state: String }, 17 | Error(Error), 18 | } 19 | 20 | #[derive(Debug, Deserialize, Serialize)] 21 | pub(super) struct Error { 22 | #[serde(rename = "error")] 23 | kind: AuthErrorKind, 24 | 25 | /// The authorization server can optionally include a human-readable description of the 26 | /// error. This parameter is intended for the developer to understand the error, and is not 27 | /// meant to be displayed to the end user. The valid characters for this parameter are the 28 | /// ASCII character set except for the double quote and backslash, specifically, hex codes 29 | /// 20-21, 23-5B and 5D-7E. 30 | #[serde(rename = "error_description")] 31 | description: Option, 32 | 33 | /// The server can also return a URL to a human-readable web page with information about 34 | /// the error. This is intended for the developer to get more information about the error, 35 | /// and is not meant to be displayed to the end user. 36 | #[serde(rename = "error_uri")] 37 | uri: Option, 38 | 39 | /// If the request contained a state parameter, the error response must also include the 40 | /// exact value from the request. The client may use this to associate this response with 41 | /// the initial request. 42 | state: String, 43 | } 44 | 45 | impl fmt::Display for Error { 46 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 47 | write!(f, "OAuth authentication flow failed") 48 | } 49 | } 50 | 51 | impl std::error::Error for Error { 52 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 53 | Some(&self.kind) 54 | } 55 | } 56 | 57 | #[derive(Debug, Deserialize, Serialize)] 58 | pub(super) enum AuthErrorKind { 59 | /// The request is missing a parameter, contains an invalid parameter, includes a parameter 60 | /// more than once, or is otherwise invalid. 61 | #[serde(rename = "invalid_request")] 62 | InvalidRequest, 63 | /// The user or authorization server denied the request. 64 | #[serde(rename = "access_denied")] 65 | AccessDenied, 66 | /// The client is not allowed to request an authorization code using this method, for example 67 | /// if a confidential client attempts to use the implicit grant type. 68 | #[serde(rename = "unauthorized_client")] 69 | UnauthorizedClient, 70 | /// The server does not support obtaining an authorization code using this method, for example 71 | /// if the authorization server never implemented the implicit grant type. 72 | #[serde(rename = "unsupported_response_type")] 73 | UnsupportedResponseType, 74 | /// The requested scope is invalid or unknown. 75 | #[serde(rename = "invalid_scope")] 76 | InvalidScope, 77 | /// Tnstead of displaying a 500 Internal Server Error page to the user, the server can redirect with this error code. 78 | #[serde(rename = "server_error")] 79 | ServerError, 80 | /// Tf the server is undergoing maintenance, or is otherwise unavailable, this error code can be returned instead of responding with a 503 Service Unavailable status code. 81 | #[serde(rename = "temporarily_unavailable")] 82 | TemporarilyUnavailable, 83 | } 84 | 85 | impl fmt::Display for AuthErrorKind { 86 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 87 | match self { 88 | AuthErrorKind::AccessDenied => write!(f, "you didn't allow access"), 89 | AuthErrorKind::InvalidRequest 90 | | AuthErrorKind::UnauthorizedClient 91 | | AuthErrorKind::UnsupportedResponseType 92 | | AuthErrorKind::InvalidScope => write!(f, "the code is wrong"), 93 | AuthErrorKind::ServerError => write!(f, "the twitter api broke"), 94 | AuthErrorKind::TemporarilyUnavailable => write!(f, "the twitter api is down"), 95 | } 96 | } 97 | } 98 | 99 | impl std::error::Error for AuthErrorKind {} 100 | 101 | /// Starts a single-request server to receive an OAuth redirect. 102 | /// 103 | /// `.0` is the URL to use for the redirect, and `.1` is a oneshot channel that the OAuth redirect 104 | /// data will be sent to when the user is redirected there. 105 | pub(super) async fn redirect_server( 106 | ) -> anyhow::Result<(RedirectUrl, tokio::sync::oneshot::Receiver)> { 107 | let (tx, rx) = tokio::sync::oneshot::channel(); 108 | let (shut, down) = tokio::sync::oneshot::channel(); 109 | 110 | let tx = Arc::new(Mutex::new(Some((tx, shut)))); 111 | 112 | // build our application with a route 113 | let app = Router::new().route( 114 | "/callback", 115 | get({ 116 | let tx = Arc::clone(&tx); 117 | |Query(token): Query| async move { 118 | let (tx, shut) = tx 119 | .lock() 120 | .expect("no lock poisoning") 121 | .take() 122 | .expect("only called once"); 123 | let _ = tx.send(token); 124 | let _ = shut.send(()); 125 | (StatusCode::CREATED, "Please return to the CLI") 126 | } 127 | }), 128 | ); 129 | 130 | // XXX: would be nice to not hard-code the port here, but Twitter's redirect allow-listing 131 | // doesn't allow wildcard ports on localhost. 132 | let addr = SocketAddr::from(([127, 0, 0, 1], 8180)); 133 | let server = axum::Server::bind(&addr).serve(app.into_make_service()); 134 | let addr = server.local_addr(); 135 | let redirect_addr = RedirectUrl::new(format!("http://127.0.0.1:{}/callback", addr.port())) 136 | .context("construct local redirect address")?; 137 | tokio::spawn(async move { 138 | if let Err(e) = server 139 | .with_graceful_shutdown(async move { 140 | down.await.ok(); 141 | }) 142 | .await 143 | { 144 | eprintln!("{}", e); 145 | } 146 | }); 147 | 148 | Ok((redirect_addr, rx)) 149 | } 150 | 151 | // This is `oauth2::reqwest::async_http_client`, except it re-uses an existing `reqwest::Client`. 152 | pub(super) async fn async_client_request( 153 | client: &reqwest::Client, 154 | request: HttpRequest, 155 | ) -> Result> { 156 | use oauth2::reqwest::Error; 157 | 158 | let mut request_builder = client 159 | .request(request.method, request.url.as_str()) 160 | .body(request.body); 161 | for (name, value) in &request.headers { 162 | request_builder = request_builder.header(name.as_str(), value.as_bytes()); 163 | } 164 | let request = request_builder.build().map_err(Error::Reqwest)?; 165 | 166 | let response = client.execute(request).await.map_err(Error::Reqwest)?; 167 | 168 | let status_code = response.status(); 169 | let headers = response.headers().to_owned(); 170 | let chunks = response.bytes().await.map_err(Error::Reqwest)?; 171 | Ok(HttpResponse { 172 | status_code, 173 | headers, 174 | body: chunks.to_vec(), 175 | }) 176 | } 177 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use futures_util::StreamExt; 3 | use indicatif::ProgressBar; 4 | use oauth2::basic::BasicClient; 5 | use oauth2::{AuthUrl, AuthorizationCode, CsrfToken, PkceCodeChallenge, Scope, TokenUrl}; 6 | use serde::{Deserialize, Serialize}; 7 | use tower::{Service, ServiceExt}; 8 | 9 | mod oauth; 10 | 11 | /// A client that knows how to authenticate Twitter API requests. 12 | #[derive(Debug, Clone)] 13 | struct RawClient { 14 | http: reqwest::Client, 15 | oauth: 16 | oauth2::StandardTokenResponse, 17 | } 18 | 19 | impl RawClient { 20 | async fn new(client_id: oauth2::ClientId) -> anyhow::Result { 21 | // Stand up a localhost server to receive the OAuth redirect. 22 | let (redirect, auth) = oauth::redirect_server() 23 | .await 24 | .context("start auth callback server")?; 25 | 26 | // OAuth time! 27 | let client = BasicClient::new( 28 | client_id.clone(), 29 | None, 30 | AuthUrl::new("https://twitter.com/i/oauth2/authorize".to_string())?, 31 | Some(TokenUrl::new( 32 | "https://api.twitter.com/2/oauth2/token".to_string(), 33 | )?), 34 | ) 35 | .set_redirect_uri(redirect); 36 | let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); 37 | let (auth_url, csrf_token) = client 38 | .authorize_url(|| CsrfToken::new_random()) 39 | .add_scope(Scope::new("tweet.read".to_string())) 40 | .add_scope(Scope::new("users.read".to_string())) 41 | .add_scope(Scope::new("follows.read".to_string())) 42 | .set_pkce_challenge(pkce_challenge) 43 | .url(); 44 | 45 | // Now the user needs to auth us, so open that in their browser. Once they click authorize 46 | // (or not), the localhost webserver will catch the redirect and we'll have the 47 | // authorization code that we can then exchange for a token. 48 | open::that(auth_url.to_string()).context("forward to Twitter for authorization")?; 49 | let authorization_code = match auth.await.context("oauth callback is called")? { 50 | oauth::Redirect::Authorized { state, .. } if &*state != csrf_token.secret() => { 51 | anyhow::bail!("bad csrf token") 52 | } 53 | oauth::Redirect::Authorized { code, .. } => code, 54 | oauth::Redirect::Error(e) => anyhow::bail!(e), 55 | }; 56 | 57 | // Exchange the one-time auth code for a longer-lived multi-use auth token. 58 | // XXX: refresh token after 2h? request offline.access? spawn that? 59 | // https://developer.twitter.com/en/docs/authentication/oauth-2-0/user-access-token 60 | let http = reqwest::Client::new(); 61 | let token_response = client 62 | .exchange_code(AuthorizationCode::new(authorization_code)) 63 | .set_pkce_verifier(pkce_verifier) 64 | // Twitter's API requires we supply this. 65 | .add_extra_param("client_id", client_id.as_str()) 66 | .request_async(|req| oauth::async_client_request(&http, req)) 67 | .await; 68 | let token = match token_response { 69 | Ok(token) => Ok(token), 70 | Err(oauth2::RequestTokenError::ServerResponse(r)) => { 71 | let e = Err(anyhow::anyhow!(r.error().clone())); 72 | match (r.error_description(), r.error_uri()) { 73 | (Some(desc), Some(url)) => { 74 | e.context(url.to_string()).context(desc.to_string()).into() 75 | } 76 | (Some(desc), None) => e.context(desc.to_string()).into(), 77 | (None, Some(url)) => e.context(url.to_string()).into(), 78 | (None, None) => e, 79 | } 80 | } 81 | Err(e) => Err(anyhow::anyhow!(e)), 82 | } 83 | .context("exchange oauth code")?; 84 | 85 | Ok(Self { http, oauth: token }) 86 | } 87 | } 88 | 89 | impl tower::Service for RawClient { 90 | type Response = reqwest::Response; 91 | type Error = anyhow::Error; 92 | type Future = 93 | std::pin::Pin>>>; 94 | 95 | fn poll_ready( 96 | &mut self, 97 | _: &mut std::task::Context<'_>, 98 | ) -> std::task::Poll> { 99 | std::task::Poll::Ready(Ok(())) 100 | } 101 | 102 | fn call(&mut self, req: reqwest::RequestBuilder) -> Self::Future { 103 | use oauth2::TokenResponse; 104 | let fut = req.bearer_auth(self.oauth.access_token().secret()).send(); 105 | Box::pin(async move { Ok(fut.await.context("request")?) }) 106 | } 107 | } 108 | 109 | /// Retry policy that knows to look for Twitter's special HTTP reply + header. 110 | /// 111 | #[derive(Copy, Clone)] 112 | struct TwitterRateLimitPolicy; 113 | impl tower::retry::Policy 114 | for TwitterRateLimitPolicy 115 | { 116 | type Future = std::pin::Pin>>; 117 | 118 | fn retry( 119 | &self, 120 | _: &reqwest::RequestBuilder, 121 | result: Result<&reqwest::Response, &anyhow::Error>, 122 | ) -> Option { 123 | let r = match result { 124 | Err(_) => return None, 125 | Ok(r) if r.status() == reqwest::StatusCode::TOO_MANY_REQUESTS => r, 126 | Ok(_) => return None, 127 | }; 128 | 129 | let reset = r 130 | .headers() 131 | .get("x-rate-limit-reset") 132 | .expect("Twitter promised"); 133 | let reset: u64 = reset 134 | .to_str() 135 | .expect("x-rate-limit-reset as str") 136 | .parse() 137 | .expect("x-rate-limit-reset is a number"); 138 | let time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(reset); 139 | 140 | Some(Box::pin(async move { 141 | match time.duration_since(std::time::SystemTime::now()) { 142 | Ok(d) if d.as_secs() > 1 => { 143 | tokio::time::sleep(d).await; 144 | } 145 | _ => { 146 | // Not worth waiting -- can just retry immediately. 147 | } 148 | } 149 | Self 150 | })) 151 | } 152 | 153 | fn clone_request(&self, req: &reqwest::RequestBuilder) -> Option { 154 | req.try_clone() 155 | } 156 | } 157 | 158 | /// A Twitter API client that authenticates requests and respects rate limitations. 159 | /// 160 | /// Note that this client does not try to proactively follow rate limits, since the limits depend 161 | /// on the endpoint, and this is generic over all endpoints. It's up to the caller (see 162 | /// page_at_a_time! for an instance of this) to wrap the inner service in an appropriate-limited 163 | /// `tower::limit::RateLimit` as needed for repeated requests. 164 | pub struct Client(tower::retry::Retry); 165 | 166 | macro_rules! page_at_a_time { 167 | ($this:ident, $msg:expr, $ids:ident, $pagesize:literal, $rate:expr, $url:literal, $t:ty) => {{ 168 | let mut ids = $ids.into_iter(); 169 | let n = ids.len(); 170 | let bar = ProgressBar::new(n as u64) 171 | .with_style( 172 | indicatif::ProgressStyle::default_bar() 173 | .template("{msg:>15} {bar:40} {percent:>3}% [{elapsed}]"), 174 | ) 175 | .with_message($msg); 176 | let mut all: Vec<$t> = Vec::with_capacity(n); 177 | let mut svc = tower::limit::RateLimit::new(&mut $this.0, $rate); 178 | let mut futs = futures_util::stream::FuturesUnordered::new(); 179 | for page in 0.. { 180 | const PAGE_SIZE: usize = $pagesize; 181 | let i = page * PAGE_SIZE; 182 | if i >= n { 183 | break; 184 | } 185 | let mut idsstr = String::new(); 186 | for id in (&mut ids).take(PAGE_SIZE) { 187 | use std::fmt::Write; 188 | if idsstr.is_empty() { 189 | write!(&mut idsstr, "{}", id) 190 | } else { 191 | write!(&mut idsstr, ",{}", id) 192 | } 193 | .expect("this is fine"); 194 | } 195 | let url = format!($url, idsstr); 196 | let req = svc.get_mut().get_mut().http.get(&url); 197 | loop { 198 | // The service may not be ready until we make progress on one of the in-flight 199 | // requests, so make sure to drive those forward too. 200 | tokio::select! { 201 | ready = svc.ready() => { 202 | let _ = ready.context("Service::poll_ready")?; 203 | break; 204 | } 205 | chunk = futs.next(), if !futs.is_empty() => { 206 | let chunk: anyhow::Result> = chunk.expect("!futs.is_empty()"); 207 | all.extend(chunk.context("grab next chunk")?); 208 | } 209 | }; 210 | } 211 | let res = tower::Service::call(&mut svc, req); 212 | let bar = bar.clone(); 213 | futs.push(async move { 214 | let data: Vec<$t> = Self::parse( 215 | res.await 216 | .with_context(|| format!("Service::call('{}')", url))?, 217 | ) 218 | .await 219 | .with_context(|| format!("parse('{}')", url))? 220 | .0; 221 | bar.inc(data.len() as u64); 222 | Ok(data) 223 | }); 224 | } 225 | 226 | while let Some(chunk) = futs.next().await.transpose().context("grab chunks")? { 227 | all.extend(chunk); 228 | } 229 | bar.finish(); 230 | 231 | Ok(all) 232 | }}; 233 | } 234 | 235 | impl Client { 236 | pub async fn new(client_id: oauth2::ClientId) -> anyhow::Result { 237 | RawClient::new(client_id) 238 | .await 239 | .map(|svc| tower::retry::Retry::new(TwitterRateLimitPolicy, svc)) 240 | .map(Self) 241 | } 242 | 243 | pub async fn whoami(&mut self) -> anyhow::Result { 244 | let req = self 245 | .0 246 | .get_mut() 247 | .http 248 | .get("https://api.twitter.com/2/users/me"); 249 | let data: WhoAmI = Self::parse( 250 | self.0 251 | .ready() 252 | .await 253 | .context("Service::poll_ready")? 254 | .call(req) 255 | .await 256 | .context("Service::call")?, 257 | ) 258 | .await 259 | .context("parse whoami")? 260 | .0; 261 | Ok(data) 262 | } 263 | 264 | pub async fn tweets(&mut self, ids: I) -> anyhow::Result> 265 | where 266 | I: IntoIterator, 267 | I::IntoIter: ExactSizeIterator, 268 | { 269 | page_at_a_time!( 270 | self, 271 | "Fetch tweets", 272 | ids, 273 | 100, 274 | tower::limit::rate::Rate::new(900, std::time::Duration::from_secs(15 * 60)), 275 | "https://api.twitter.com/2/tweets?tweet.fields=id,created_at,public_metrics&ids={}", 276 | Tweet 277 | ) 278 | } 279 | 280 | pub async fn users(&mut self, ids: I) -> anyhow::Result> 281 | where 282 | I: IntoIterator, 283 | I::IntoIter: ExactSizeIterator, 284 | { 285 | page_at_a_time!( 286 | self, 287 | "Fetch followers", 288 | ids, 289 | 100, 290 | tower::limit::rate::Rate::new(900, std::time::Duration::from_secs(15 * 60)), 291 | "https://api.twitter.com/2/users?user.fields=username,public_metrics&ids={}", 292 | User 293 | ) 294 | } 295 | 296 | async fn parse(res: reqwest::Response) -> anyhow::Result<(T, Option)> 297 | where 298 | T: serde::de::DeserializeOwned, 299 | { 300 | // This is th general structure of all Twitter API responses. 301 | #[derive(Debug, Deserialize)] 302 | struct Data { 303 | data: T, 304 | meta: Option, 305 | } 306 | 307 | // We _could_ do: 308 | // let data: Data = res.json().await.context("parse")?; 309 | // but that would make for unhelpful error messages if parsing fails, so we do: 310 | let data = res.text().await.context("get body")?; 311 | let data: Data = serde_json::from_str(&data) 312 | .with_context(|| data) 313 | .context("parse")?; 314 | Ok((data.data, data.meta)) 315 | } 316 | } 317 | 318 | /* 319 | pub async fn from_pages( 320 | http: &reqwest::Client, 321 | token: &TR, 322 | url: impl Into, 323 | mut map: F, 324 | ) -> anyhow::Result 325 | where 326 | T: serde::de::DeserializeOwned, 327 | TT: oauth2::TokenType, 328 | TR: oauth2::TokenResponse, 329 | F: FnMut(T, &Meta) -> FT, 330 | C: Default + Extend, 331 | { 332 | let mut all: C = Default::default(); 333 | let mut next = None::; 334 | let url = url.into(); 335 | loop { 336 | let url = match next.as_ref() { 337 | None => url.clone(), 338 | Some(p) => { 339 | let mut url = url.clone(); 340 | url.query_pairs_mut().append_pair("pagination_token", p); 341 | url 342 | } 343 | }; 344 | 345 | let (page, meta): (Vec, _) = grab(http, token, url.to_string()) 346 | .await 347 | .context("followers")?; 348 | let meta = meta.expect("always meta for this"); 349 | 350 | assert_eq!(page.len(), meta.results); 351 | all.extend(page.into_iter().map(|t| (map)(t, &meta))); 352 | if meta.next.is_none() { 353 | break; 354 | } 355 | next = meta.next; 356 | } 357 | Ok(all) 358 | } 359 | */ 360 | 361 | #[derive(Debug, Deserialize)] 362 | pub struct WhoAmI { 363 | pub id: String, 364 | pub username: String, 365 | } 366 | 367 | #[derive(Debug, Serialize, Deserialize)] 368 | pub struct PublicTweetMetrics { 369 | #[serde(rename = "retweet_count")] 370 | pub retweets: usize, 371 | #[serde(rename = "reply_count")] 372 | pub replies: usize, 373 | #[serde(rename = "like_count")] 374 | pub likes: usize, 375 | #[serde(rename = "quote_count")] 376 | pub quotations: usize, 377 | } 378 | 379 | #[derive(Debug, Serialize, Deserialize)] 380 | pub struct Tweet { 381 | #[serde(rename = "id", with = "u64_but_str")] 382 | pub id: u64, 383 | #[serde(rename = "created_at", with = "time::serde::rfc3339")] 384 | pub created: time::OffsetDateTime, 385 | #[serde(rename = "public_metrics")] 386 | pub metrics: PublicTweetMetrics, 387 | // not reading in text: String here 388 | // would be great to read non_public_metrics, but those aren't available >30 days 389 | } 390 | 391 | impl Tweet { 392 | pub fn goodness(&self) -> usize { 393 | self.metrics.likes 394 | + 2 * self.metrics.retweets 395 | + 3 * self.metrics.quotations 396 | + self.metrics.replies / 2 397 | } 398 | } 399 | 400 | #[derive(Debug, Serialize, Deserialize)] 401 | pub struct PublicUserMetrics { 402 | #[serde(rename = "followers_count")] 403 | pub followers: usize, 404 | #[serde(rename = "following_count")] 405 | pub following: usize, 406 | } 407 | 408 | #[derive(Debug, Serialize, Deserialize)] 409 | pub struct User { 410 | pub username: String, 411 | #[serde(rename = "public_metrics")] 412 | pub metrics: PublicUserMetrics, 413 | } 414 | 415 | // Keeping this around for if I ever need to add pagination. 416 | #[derive(Debug, Deserialize)] 417 | #[allow(dead_code)] 418 | struct Meta { 419 | #[serde(rename = "result_count")] 420 | results: usize, 421 | #[serde(rename = "next_token")] 422 | next: Option, 423 | } 424 | 425 | mod u64_but_str { 426 | use std::fmt::Display; 427 | 428 | use serde::{de, Deserialize, Deserializer, Serializer}; 429 | 430 | pub fn serialize(value: &T, serializer: S) -> Result 431 | where 432 | T: Display, 433 | S: Serializer, 434 | { 435 | serializer.collect_str(value) 436 | } 437 | 438 | pub fn deserialize<'de, D>(deserializer: D) -> Result 439 | where 440 | D: Deserializer<'de>, 441 | { 442 | let s = String::deserialize(deserializer)?; 443 | s.parse().map_err(de::Error::custom) 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use clap::Parser; 3 | use oauth2::ClientId; 4 | use ornithology_cli::{api, archive}; 5 | use rand::prelude::*; 6 | use serde::{Deserialize, Serialize}; 7 | use std::{ 8 | collections::HashMap, 9 | path::{Path, PathBuf}, 10 | }; 11 | 12 | /// Twitter history introspection based on archive exports. 13 | #[derive(Parser, Debug)] 14 | #[clap(author, version, about, long_about = None)] 15 | struct Args { 16 | /// Number of "top" items to show for each tweet statistic. 17 | #[clap(short = 'n', long, default_value_t = 5)] 18 | top_tweets: u8, 19 | 20 | /// Number of "top" items to show for each follower statistic. 21 | #[clap(long, default_value_t = 10)] 22 | top_followers: u8, 23 | 24 | /// Ensure that fresh metrics are loaded for every tweet and user. 25 | /// 26 | /// This requires talking to the Twitter API, and can be _much_ slower especially if you have a 27 | /// lot of tweets or followers and start hitting Twitter's API rate limits: 28 | /// . 29 | /// 30 | /// The first time you run this program, it _must_ load all the data, but on subsequent 31 | /// invocations it'll use cached data unless this flag is passed. 32 | #[clap(long)] 33 | fresh: bool, 34 | 35 | /// Path to your Twitter archive .zip file. 36 | /// 37 | /// To get this file, follow the instructions at 38 | /// . 39 | /// It takes about 24h to get the archive after you submit the request, so come back later if 40 | /// you don't yet have said file :) 41 | archive: PathBuf, 42 | } 43 | 44 | #[tokio::main] 45 | async fn main() -> anyhow::Result<()> { 46 | let args = Args::parse(); 47 | let toptn = args.top_tweets as usize; 48 | let topfn = args.top_followers as usize; 49 | let archive = &*Box::leak(args.archive.into_boxed_path()); 50 | 51 | let Loaded { 52 | me, 53 | old_rt_ids, 54 | mut tweets, 55 | mut followers, 56 | } = load(!args.fresh, archive).await.context("load dataset")?; 57 | 58 | let mut lists_of_tweets: HashMap<&'static str, Vec> = HashMap::new(); 59 | 60 | // It's fun to surface RTs that people may have forgotten about. 61 | if !old_rt_ids.is_empty() { 62 | // XXX: can theoretically look up tweet originals using 63 | // expansions=referenced_tweets.id and look for type=retweeted 64 | println!("remember these old retweets:"); 65 | let mut rng = rand::thread_rng(); 66 | let entry = lists_of_tweets 67 | .entry("old_rts") 68 | .or_insert_with(|| Vec::with_capacity(toptn)); 69 | for old_rt_id in old_rt_ids.choose_multiple(&mut rng, toptn) { 70 | println!("https://twitter.com/{}/status/{}", me, old_rt_id); 71 | entry.push(old_rt_id.to_string()); 72 | } 73 | } 74 | 75 | // Metrics that depend on the time-based state. 76 | // For example, how good was this tweet relative to other tweets at/up until that point in 77 | // time. The idea being that what makes a tweet "notable" isn't "does it have a lot of likes", 78 | // but "did it get a lot of likes relative to other tweets you twote back then"? 79 | { 80 | // For these, we need the tweet list to be sorted by time, so we use a block that sorts and 81 | // then borrows as read-only: 82 | tweets.sort_unstable_by_key(|t| t.created); 83 | let tweets = &tweets[..]; 84 | fn find_notable( 85 | tweets: &[api::Tweet], 86 | mut metric: impl FnMut(&api::Tweet) -> usize, 87 | at_least: f64, 88 | at_least_x: f64, 89 | ) -> Vec<(f64, f64, usize)> { 90 | let mut avg = 0.0f64; 91 | let mut notable = Vec::new(); 92 | for (i, t) in tweets.iter().enumerate() { 93 | let floor = (at_least_x * avg).max(at_least); 94 | let g = (metric)(t) as f64; 95 | if g > floor { 96 | notable.push((g / avg, avg, i)); 97 | } 98 | avg = 0.5 * g + 0.5 * avg; 99 | } 100 | notable.sort_unstable_by_key(|(x, _, i)| { 101 | // Reverse because we want the top ones to come first, not last. 102 | std::cmp::Reverse(((1000.0 * x).round() as usize, (metric)(&tweets[*i]))) 103 | }); 104 | notable 105 | } 106 | 107 | println!("notable tweets:"); 108 | let notable = find_notable(&tweets, |t| t.goodness(), 10.0, 2.0); 109 | let entry = lists_of_tweets 110 | .entry("notable_tweets") 111 | .or_insert_with(|| Vec::with_capacity(toptn)); 112 | for (_, avg, i) in notable.into_iter().take(toptn) { 113 | let tweet = &tweets[i]; 114 | println!( 115 | "https://twitter.com/{}/status/{} ({} likes/{} rts when avg was {:.2})", 116 | me, tweet.id, tweet.metrics.likes, tweet.metrics.retweets, avg 117 | ); 118 | entry.push(tweet.id.to_string()); 119 | } 120 | 121 | println!("talked-about tweets:"); 122 | let entry = lists_of_tweets 123 | .entry("talked_about_tweets") 124 | .or_insert_with(|| Vec::with_capacity(toptn)); 125 | let notable = find_notable( 126 | &tweets, 127 | |t| 2 * t.metrics.quotations + t.metrics.replies, 128 | 10.0, 129 | 2.0, 130 | ); 131 | for (_, avg, i) in notable.into_iter().take(toptn) { 132 | let tweet = &tweets[i]; 133 | println!( 134 | "https://twitter.com/{}/status/{} ({} quotes + {} replies when avg as {:.2})", 135 | me, tweet.id, tweet.metrics.quotations, tweet.metrics.replies, avg 136 | ); 137 | entry.push(tweet.id.to_string()); 138 | } 139 | 140 | println!("over-shared tweets:"); 141 | let entry = lists_of_tweets 142 | .entry("over_shared_tweets") 143 | .or_insert_with(|| Vec::with_capacity(toptn)); 144 | let notable = find_notable( 145 | &tweets, 146 | |t| 2 * t.metrics.quotations + t.metrics.retweets, 147 | 10.0, 148 | 2.0, 149 | ); 150 | for (_, avg, i) in notable.into_iter().take(toptn) { 151 | let tweet = &tweets[i]; 152 | println!( 153 | "https://twitter.com/{}/status/{} ({} quotes + {} retweets when avg as {:.2})", 154 | me, tweet.id, tweet.metrics.quotations, tweet.metrics.retweets, avg 155 | ); 156 | entry.push(tweet.id.to_string()); 157 | } 158 | } 159 | 160 | // Now to the boring "best/most of all time" bits: 161 | println!("top tweets:"); 162 | let entry = lists_of_tweets 163 | .entry("top_tweets") 164 | .or_insert_with(|| Vec::with_capacity(toptn)); 165 | tweets.sort_unstable_by_key(|t| t.goodness()); 166 | for tweet in tweets.iter().rev().take(toptn) { 167 | println!( 168 | "https://twitter.com/{}/status/{} ({} likes/{} rts)", 169 | me, tweet.id, tweet.metrics.likes, tweet.metrics.retweets 170 | ); 171 | entry.push(tweet.id.to_string()); 172 | } 173 | 174 | println!("most talked-about tweets:"); 175 | let entry = lists_of_tweets 176 | .entry("most_talked_about_tweets") 177 | .or_insert_with(|| Vec::with_capacity(toptn)); 178 | tweets.sort_unstable_by_key(|t| 2 * t.metrics.quotations + t.metrics.replies); 179 | for tweet in tweets.iter().rev().take(toptn) { 180 | println!( 181 | "https://twitter.com/{}/status/{} ({} quotes/{} replies)", 182 | me, tweet.id, tweet.metrics.quotations, tweet.metrics.replies 183 | ); 184 | entry.push(tweet.id.to_string()); 185 | } 186 | 187 | println!("most shared tweets:"); 188 | let entry = lists_of_tweets 189 | .entry("most_shared_tweets") 190 | .or_insert_with(|| Vec::with_capacity(toptn)); 191 | tweets.sort_unstable_by_key(|t| 2 * t.metrics.quotations + t.metrics.retweets); 192 | for tweet in tweets.iter().rev().take(toptn) { 193 | println!( 194 | "https://twitter.com/{}/status/{} ({} quotes/{} rts)", 195 | me, tweet.id, tweet.metrics.quotations, tweet.metrics.retweets 196 | ); 197 | entry.push(tweet.id.to_string()); 198 | } 199 | 200 | // Then we move on to follower stats. 201 | // First the obvious one: 202 | println!("top followers:"); 203 | let entry = lists_of_tweets 204 | .entry("top_followers") 205 | .or_insert_with(|| Vec::with_capacity(topfn)); 206 | followers.sort_unstable_by_key(|f| f.metrics.followers); 207 | for follower in followers.iter().rev().take(topfn) { 208 | println!( 209 | "https://twitter.com/{} ({} followers)", 210 | follower.username, follower.metrics.followers 211 | ); 212 | entry.push(follower.username.to_string()); 213 | } 214 | 215 | // What is a "neat" follower? 216 | // Well, we want to figure out who are "big" accounts that follow us, and where it is _notable_ 217 | // that they do. For example, if an account with 1MM followers follow me, but it follows 1MM 218 | // other accounts, it's not that interesting (they probably just follow everyone back). But if 219 | // they follow _just_ me, that's very neat. 220 | println!("neat followers:"); 221 | let entry = lists_of_tweets 222 | .entry("neat_followers") 223 | .or_insert_with(|| Vec::with_capacity(topfn)); 224 | followers 225 | .sort_unstable_by_key(|f| f.metrics.followers as isize - 10 * f.metrics.following as isize); 226 | for follower in followers.iter().rev().take(topfn) { 227 | println!( 228 | "https://twitter.com/{} ({} followers but only following {})", 229 | follower.username, follower.metrics.followers, follower.metrics.following 230 | ); 231 | entry.push(follower.username.to_string()); 232 | } 233 | 234 | let groups = Vec::from([ 235 | ("top_tweets", "Top tweets"), 236 | ("most_talked_about_tweets", "Most talked about tweets"), 237 | ("most_shared_tweets", "Most shared tweets"), 238 | ("notable_tweets", "Notable tweets (at the time)"), 239 | ("talked_about_tweets", "Talked about tweets (at the time)"), 240 | ("over_shared_tweets", "Widely shared tweets (at the time)"), 241 | ("old_rts", "Random old retweets"), 242 | ]); 243 | for (id, _) in &groups { 244 | assert!(lists_of_tweets.contains_key(id), "{}", id); 245 | } 246 | let groups = serde_json::to_string(&groups).expect("serialize groups"); 247 | 248 | let data = serde_json::to_string(&lists_of_tweets).expect("serialize lists_of_tweets"); 249 | let html = format!( 250 | r#" 251 | 252 | 253 | 254 | 255 | @{me} ornithology 256 | 295 | 296 | 297 |
    298 |
    299 | 330 | 331 | 339 | 340 | 341 | "#, 342 | ); 343 | let f = Path::new("ornithology.html"); 344 | tokio::fs::write(&f, &html) 345 | .await 346 | .context("write ornithology.html")?; 347 | open::that(f).context("open generated page")?; 348 | 349 | // TODO: add plots, like scatter plot of time/likes (prob. include id for easy reference) 350 | 351 | Ok(()) 352 | } 353 | 354 | #[derive(Debug, Serialize, Deserialize)] 355 | struct Loaded { 356 | me: String, 357 | old_rt_ids: Vec, 358 | tweets: Vec, 359 | followers: Vec, 360 | } 361 | 362 | async fn load(use_cache: bool, archive: &'static Path) -> anyhow::Result { 363 | let cache_file = Path::new("cache.json"); 364 | if use_cache && cache_file.exists() { 365 | let s = tokio::fs::read(&cache_file) 366 | .await 367 | .with_context(|| format!("read {}", cache_file.display()))?; 368 | return Ok(serde_json::from_slice(&s) 369 | .with_context(|| format!("parse {}", cache_file.display()))?); 370 | } 371 | 372 | let (old_rt_ids, follower_ids, tweet_ids) = tokio::task::spawn_blocking(|| { 373 | let fname = Path::new(archive); 374 | let zipfile = std::fs::File::open(&fname).context("open twitter archive")?; 375 | let mut archive = zip::ZipArchive::new(zipfile).context("open twitter archive as zip")?; 376 | 377 | let followers: Vec = archive::parse( 378 | &mut archive, 379 | "data/follower.js", 380 | |archive::Follower::One { id }| Some(id), 381 | ) 382 | .context("extract follower list")?; 383 | 384 | let mut oldies = Vec::new(); 385 | let tweets: Vec = archive::parse( 386 | &mut archive, 387 | "data/tweet.js", 388 | |archive::Tweet::One { id, text }| { 389 | if text.starts_with("RT @") { 390 | oldies.push(id); 391 | None 392 | } else { 393 | Some(id) 394 | } 395 | }, 396 | ) 397 | .context("extract follower list")?; 398 | 399 | Ok::<_, anyhow::Error>((oldies, followers, tweets)) 400 | }) 401 | .await 402 | .context("spawn blocking")??; 403 | 404 | let client_id = ClientId::new("SUtlNTYydEhnVDJEOW5uSmh3Q0g6MTpjaQ".to_string()); 405 | let mut client = api::Client::new(client_id) 406 | .await 407 | .context("api::Client::new")?; 408 | 409 | // Let's first figure out which user we are 410 | let whoami = client.whoami().await.context("whoami")?; 411 | eprintln!("whoami: @{} ({})", whoami.username, whoami.id); 412 | 413 | // Now get stats about each tweet: 414 | let tweets = client.tweets(tweet_ids).await.context("fetch tweets")?; 415 | 416 | // and about each follower: 417 | let followers = client.users(follower_ids).await.context("fetch follower")?; 418 | 419 | let loaded = Loaded { 420 | me: whoami.username, 421 | old_rt_ids, 422 | tweets, 423 | followers, 424 | }; 425 | tokio::fs::write( 426 | &cache_file, 427 | &serde_json::to_vec(&loaded).context("serialize cache.json")?, 428 | ) 429 | .await 430 | .with_context(|| format!("write {}", cache_file.display()))?; 431 | 432 | Ok(loaded) 433 | } 434 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "aes" 13 | version = "0.7.5" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" 16 | dependencies = [ 17 | "cfg-if", 18 | "cipher", 19 | "cpufeatures", 20 | "opaque-debug", 21 | ] 22 | 23 | [[package]] 24 | name = "anyhow" 25 | version = "1.0.57" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" 28 | 29 | [[package]] 30 | name = "async-trait" 31 | version = "0.1.56" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" 34 | dependencies = [ 35 | "proc-macro2", 36 | "quote", 37 | "syn", 38 | ] 39 | 40 | [[package]] 41 | name = "atty" 42 | version = "0.2.14" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 45 | dependencies = [ 46 | "hermit-abi", 47 | "libc", 48 | "winapi", 49 | ] 50 | 51 | [[package]] 52 | name = "autocfg" 53 | version = "1.1.0" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 56 | 57 | [[package]] 58 | name = "axum" 59 | version = "0.5.7" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "dc47084705629d09d15060d70a8dbfce479c842303d05929ce29c74c995916ae" 62 | dependencies = [ 63 | "async-trait", 64 | "axum-core", 65 | "bitflags", 66 | "bytes", 67 | "futures-util", 68 | "http", 69 | "http-body", 70 | "hyper", 71 | "itoa", 72 | "matchit", 73 | "memchr", 74 | "mime", 75 | "percent-encoding", 76 | "pin-project-lite", 77 | "serde", 78 | "serde_json", 79 | "serde_urlencoded", 80 | "sync_wrapper", 81 | "tokio", 82 | "tower", 83 | "tower-http", 84 | "tower-layer", 85 | "tower-service", 86 | ] 87 | 88 | [[package]] 89 | name = "axum-core" 90 | version = "0.2.5" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "c2efed1c501becea07ce48118786ebcf229531d0d3b28edf224a720020d9e106" 93 | dependencies = [ 94 | "async-trait", 95 | "bytes", 96 | "futures-util", 97 | "http", 98 | "http-body", 99 | "mime", 100 | ] 101 | 102 | [[package]] 103 | name = "base64" 104 | version = "0.13.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 107 | 108 | [[package]] 109 | name = "base64ct" 110 | version = "1.0.1" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" 113 | 114 | [[package]] 115 | name = "bitflags" 116 | version = "1.3.2" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 119 | 120 | [[package]] 121 | name = "block-buffer" 122 | version = "0.10.2" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" 125 | dependencies = [ 126 | "generic-array", 127 | ] 128 | 129 | [[package]] 130 | name = "bumpalo" 131 | version = "3.10.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" 134 | 135 | [[package]] 136 | name = "byteorder" 137 | version = "1.4.3" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 140 | 141 | [[package]] 142 | name = "bytes" 143 | version = "1.1.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" 146 | 147 | [[package]] 148 | name = "bzip2" 149 | version = "0.4.3" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" 152 | dependencies = [ 153 | "bzip2-sys", 154 | "libc", 155 | ] 156 | 157 | [[package]] 158 | name = "bzip2-sys" 159 | version = "0.1.11+1.0.8" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" 162 | dependencies = [ 163 | "cc", 164 | "libc", 165 | "pkg-config", 166 | ] 167 | 168 | [[package]] 169 | name = "cc" 170 | version = "1.0.73" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 173 | dependencies = [ 174 | "jobserver", 175 | ] 176 | 177 | [[package]] 178 | name = "cfg-if" 179 | version = "1.0.0" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 182 | 183 | [[package]] 184 | name = "chrono" 185 | version = "0.4.19" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 188 | dependencies = [ 189 | "libc", 190 | "num-integer", 191 | "num-traits", 192 | "serde", 193 | "winapi", 194 | ] 195 | 196 | [[package]] 197 | name = "cipher" 198 | version = "0.3.0" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" 201 | dependencies = [ 202 | "generic-array", 203 | ] 204 | 205 | [[package]] 206 | name = "clap" 207 | version = "3.2.5" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "d53da17d37dba964b9b3ecb5c5a1f193a2762c700e6829201e645b9381c99dc7" 210 | dependencies = [ 211 | "atty", 212 | "bitflags", 213 | "clap_derive", 214 | "clap_lex", 215 | "indexmap", 216 | "once_cell", 217 | "strsim", 218 | "termcolor", 219 | "textwrap", 220 | ] 221 | 222 | [[package]] 223 | name = "clap_derive" 224 | version = "3.2.5" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "c11d40217d16aee8508cc8e5fde8b4ff24639758608e5374e731b53f85749fb9" 227 | dependencies = [ 228 | "heck", 229 | "proc-macro-error", 230 | "proc-macro2", 231 | "quote", 232 | "syn", 233 | ] 234 | 235 | [[package]] 236 | name = "clap_lex" 237 | version = "0.2.2" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613" 240 | dependencies = [ 241 | "os_str_bytes", 242 | ] 243 | 244 | [[package]] 245 | name = "console" 246 | version = "0.15.0" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" 249 | dependencies = [ 250 | "encode_unicode", 251 | "libc", 252 | "once_cell", 253 | "terminal_size", 254 | "winapi", 255 | ] 256 | 257 | [[package]] 258 | name = "constant_time_eq" 259 | version = "0.1.5" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 262 | 263 | [[package]] 264 | name = "core-foundation" 265 | version = "0.9.3" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 268 | dependencies = [ 269 | "core-foundation-sys", 270 | "libc", 271 | ] 272 | 273 | [[package]] 274 | name = "core-foundation-sys" 275 | version = "0.8.3" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 278 | 279 | [[package]] 280 | name = "cpufeatures" 281 | version = "0.2.2" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" 284 | dependencies = [ 285 | "libc", 286 | ] 287 | 288 | [[package]] 289 | name = "crc32fast" 290 | version = "1.3.2" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 293 | dependencies = [ 294 | "cfg-if", 295 | ] 296 | 297 | [[package]] 298 | name = "crossbeam-utils" 299 | version = "0.8.8" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" 302 | dependencies = [ 303 | "cfg-if", 304 | "lazy_static", 305 | ] 306 | 307 | [[package]] 308 | name = "crypto-common" 309 | version = "0.1.3" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" 312 | dependencies = [ 313 | "generic-array", 314 | "typenum", 315 | ] 316 | 317 | [[package]] 318 | name = "digest" 319 | version = "0.10.3" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" 322 | dependencies = [ 323 | "block-buffer", 324 | "crypto-common", 325 | "subtle", 326 | ] 327 | 328 | [[package]] 329 | name = "encode_unicode" 330 | version = "0.3.6" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 333 | 334 | [[package]] 335 | name = "encoding_rs" 336 | version = "0.8.31" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" 339 | dependencies = [ 340 | "cfg-if", 341 | ] 342 | 343 | [[package]] 344 | name = "fastrand" 345 | version = "1.7.0" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" 348 | dependencies = [ 349 | "instant", 350 | ] 351 | 352 | [[package]] 353 | name = "flate2" 354 | version = "1.0.24" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" 357 | dependencies = [ 358 | "crc32fast", 359 | "miniz_oxide", 360 | ] 361 | 362 | [[package]] 363 | name = "fnv" 364 | version = "1.0.7" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 367 | 368 | [[package]] 369 | name = "foreign-types" 370 | version = "0.3.2" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 373 | dependencies = [ 374 | "foreign-types-shared", 375 | ] 376 | 377 | [[package]] 378 | name = "foreign-types-shared" 379 | version = "0.1.1" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 382 | 383 | [[package]] 384 | name = "form_urlencoded" 385 | version = "1.0.1" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 388 | dependencies = [ 389 | "matches", 390 | "percent-encoding", 391 | ] 392 | 393 | [[package]] 394 | name = "futures-channel" 395 | version = "0.3.21" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" 398 | dependencies = [ 399 | "futures-core", 400 | ] 401 | 402 | [[package]] 403 | name = "futures-core" 404 | version = "0.3.21" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" 407 | 408 | [[package]] 409 | name = "futures-io" 410 | version = "0.3.21" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" 413 | 414 | [[package]] 415 | name = "futures-macro" 416 | version = "0.3.21" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" 419 | dependencies = [ 420 | "proc-macro2", 421 | "quote", 422 | "syn", 423 | ] 424 | 425 | [[package]] 426 | name = "futures-sink" 427 | version = "0.3.21" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" 430 | 431 | [[package]] 432 | name = "futures-task" 433 | version = "0.3.21" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" 436 | 437 | [[package]] 438 | name = "futures-util" 439 | version = "0.3.21" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" 442 | dependencies = [ 443 | "futures-core", 444 | "futures-io", 445 | "futures-macro", 446 | "futures-task", 447 | "memchr", 448 | "pin-project-lite", 449 | "pin-utils", 450 | "slab", 451 | ] 452 | 453 | [[package]] 454 | name = "generic-array" 455 | version = "0.14.5" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" 458 | dependencies = [ 459 | "typenum", 460 | "version_check", 461 | ] 462 | 463 | [[package]] 464 | name = "getrandom" 465 | version = "0.2.6" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" 468 | dependencies = [ 469 | "cfg-if", 470 | "js-sys", 471 | "libc", 472 | "wasi 0.10.2+wasi-snapshot-preview1", 473 | "wasm-bindgen", 474 | ] 475 | 476 | [[package]] 477 | name = "h2" 478 | version = "0.3.13" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" 481 | dependencies = [ 482 | "bytes", 483 | "fnv", 484 | "futures-core", 485 | "futures-sink", 486 | "futures-util", 487 | "http", 488 | "indexmap", 489 | "slab", 490 | "tokio", 491 | "tokio-util", 492 | "tracing", 493 | ] 494 | 495 | [[package]] 496 | name = "hashbrown" 497 | version = "0.11.2" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 500 | 501 | [[package]] 502 | name = "heck" 503 | version = "0.4.0" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 506 | 507 | [[package]] 508 | name = "hermit-abi" 509 | version = "0.1.19" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 512 | dependencies = [ 513 | "libc", 514 | ] 515 | 516 | [[package]] 517 | name = "hmac" 518 | version = "0.12.1" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 521 | dependencies = [ 522 | "digest", 523 | ] 524 | 525 | [[package]] 526 | name = "http" 527 | version = "0.2.8" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" 530 | dependencies = [ 531 | "bytes", 532 | "fnv", 533 | "itoa", 534 | ] 535 | 536 | [[package]] 537 | name = "http-body" 538 | version = "0.4.5" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 541 | dependencies = [ 542 | "bytes", 543 | "http", 544 | "pin-project-lite", 545 | ] 546 | 547 | [[package]] 548 | name = "http-range-header" 549 | version = "0.3.0" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" 552 | 553 | [[package]] 554 | name = "httparse" 555 | version = "1.7.1" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" 558 | 559 | [[package]] 560 | name = "httpdate" 561 | version = "1.0.2" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 564 | 565 | [[package]] 566 | name = "hyper" 567 | version = "0.14.19" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" 570 | dependencies = [ 571 | "bytes", 572 | "futures-channel", 573 | "futures-core", 574 | "futures-util", 575 | "h2", 576 | "http", 577 | "http-body", 578 | "httparse", 579 | "httpdate", 580 | "itoa", 581 | "pin-project-lite", 582 | "socket2", 583 | "tokio", 584 | "tower-service", 585 | "tracing", 586 | "want", 587 | ] 588 | 589 | [[package]] 590 | name = "hyper-rustls" 591 | version = "0.23.0" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" 594 | dependencies = [ 595 | "http", 596 | "hyper", 597 | "rustls", 598 | "tokio", 599 | "tokio-rustls", 600 | ] 601 | 602 | [[package]] 603 | name = "hyper-tls" 604 | version = "0.5.0" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 607 | dependencies = [ 608 | "bytes", 609 | "hyper", 610 | "native-tls", 611 | "tokio", 612 | "tokio-native-tls", 613 | ] 614 | 615 | [[package]] 616 | name = "idna" 617 | version = "0.2.3" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 620 | dependencies = [ 621 | "matches", 622 | "unicode-bidi", 623 | "unicode-normalization", 624 | ] 625 | 626 | [[package]] 627 | name = "indexmap" 628 | version = "1.8.2" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" 631 | dependencies = [ 632 | "autocfg", 633 | "hashbrown", 634 | ] 635 | 636 | [[package]] 637 | name = "indicatif" 638 | version = "0.16.2" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b" 641 | dependencies = [ 642 | "console", 643 | "lazy_static", 644 | "number_prefix", 645 | "regex", 646 | ] 647 | 648 | [[package]] 649 | name = "instant" 650 | version = "0.1.12" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 653 | dependencies = [ 654 | "cfg-if", 655 | ] 656 | 657 | [[package]] 658 | name = "ipnet" 659 | version = "2.5.0" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" 662 | 663 | [[package]] 664 | name = "itoa" 665 | version = "1.0.2" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" 668 | 669 | [[package]] 670 | name = "jobserver" 671 | version = "0.1.24" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" 674 | dependencies = [ 675 | "libc", 676 | ] 677 | 678 | [[package]] 679 | name = "js-sys" 680 | version = "0.3.57" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" 683 | dependencies = [ 684 | "wasm-bindgen", 685 | ] 686 | 687 | [[package]] 688 | name = "lazy_static" 689 | version = "1.4.0" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 692 | 693 | [[package]] 694 | name = "libc" 695 | version = "0.2.126" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 698 | 699 | [[package]] 700 | name = "lock_api" 701 | version = "0.4.7" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" 704 | dependencies = [ 705 | "autocfg", 706 | "scopeguard", 707 | ] 708 | 709 | [[package]] 710 | name = "log" 711 | version = "0.4.17" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 714 | dependencies = [ 715 | "cfg-if", 716 | ] 717 | 718 | [[package]] 719 | name = "matches" 720 | version = "0.1.9" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" 723 | 724 | [[package]] 725 | name = "matchit" 726 | version = "0.5.0" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" 729 | 730 | [[package]] 731 | name = "memchr" 732 | version = "2.5.0" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 735 | 736 | [[package]] 737 | name = "mime" 738 | version = "0.3.16" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 741 | 742 | [[package]] 743 | name = "miniz_oxide" 744 | version = "0.5.3" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" 747 | dependencies = [ 748 | "adler", 749 | ] 750 | 751 | [[package]] 752 | name = "mio" 753 | version = "0.8.3" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" 756 | dependencies = [ 757 | "libc", 758 | "log", 759 | "wasi 0.11.0+wasi-snapshot-preview1", 760 | "windows-sys", 761 | ] 762 | 763 | [[package]] 764 | name = "native-tls" 765 | version = "0.2.10" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" 768 | dependencies = [ 769 | "lazy_static", 770 | "libc", 771 | "log", 772 | "openssl", 773 | "openssl-probe", 774 | "openssl-sys", 775 | "schannel", 776 | "security-framework", 777 | "security-framework-sys", 778 | "tempfile", 779 | ] 780 | 781 | [[package]] 782 | name = "num-integer" 783 | version = "0.1.45" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 786 | dependencies = [ 787 | "autocfg", 788 | "num-traits", 789 | ] 790 | 791 | [[package]] 792 | name = "num-traits" 793 | version = "0.2.15" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 796 | dependencies = [ 797 | "autocfg", 798 | ] 799 | 800 | [[package]] 801 | name = "num_cpus" 802 | version = "1.13.1" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 805 | dependencies = [ 806 | "hermit-abi", 807 | "libc", 808 | ] 809 | 810 | [[package]] 811 | name = "num_threads" 812 | version = "0.1.6" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" 815 | dependencies = [ 816 | "libc", 817 | ] 818 | 819 | [[package]] 820 | name = "number_prefix" 821 | version = "0.4.0" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 824 | 825 | [[package]] 826 | name = "oauth2" 827 | version = "4.2.0" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "c3bd7d544f02ae0fa9e06137962703d043870d7ad6e6d44786d6a5f20679b2c9" 830 | dependencies = [ 831 | "base64", 832 | "chrono", 833 | "getrandom", 834 | "http", 835 | "rand", 836 | "reqwest", 837 | "serde", 838 | "serde_json", 839 | "serde_path_to_error", 840 | "sha2", 841 | "thiserror", 842 | "url", 843 | ] 844 | 845 | [[package]] 846 | name = "once_cell" 847 | version = "1.12.0" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" 850 | 851 | [[package]] 852 | name = "opaque-debug" 853 | version = "0.3.0" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 856 | 857 | [[package]] 858 | name = "open" 859 | version = "3.0.1" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "360bcc8316bf6363aa3954c3ccc4de8add167b087e0259190a043c9514f910fe" 862 | dependencies = [ 863 | "pathdiff", 864 | "windows-sys", 865 | ] 866 | 867 | [[package]] 868 | name = "openssl" 869 | version = "0.10.40" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e" 872 | dependencies = [ 873 | "bitflags", 874 | "cfg-if", 875 | "foreign-types", 876 | "libc", 877 | "once_cell", 878 | "openssl-macros", 879 | "openssl-sys", 880 | ] 881 | 882 | [[package]] 883 | name = "openssl-macros" 884 | version = "0.1.0" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" 887 | dependencies = [ 888 | "proc-macro2", 889 | "quote", 890 | "syn", 891 | ] 892 | 893 | [[package]] 894 | name = "openssl-probe" 895 | version = "0.1.5" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 898 | 899 | [[package]] 900 | name = "openssl-sys" 901 | version = "0.9.74" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1" 904 | dependencies = [ 905 | "autocfg", 906 | "cc", 907 | "libc", 908 | "pkg-config", 909 | "vcpkg", 910 | ] 911 | 912 | [[package]] 913 | name = "ornithology-cli" 914 | version = "0.1.1" 915 | dependencies = [ 916 | "anyhow", 917 | "axum", 918 | "clap", 919 | "futures-util", 920 | "indicatif", 921 | "oauth2", 922 | "open", 923 | "rand", 924 | "reqwest", 925 | "serde", 926 | "serde_json", 927 | "serde_urlencoded", 928 | "time", 929 | "tokio", 930 | "tower", 931 | "url", 932 | "zip", 933 | ] 934 | 935 | [[package]] 936 | name = "os_str_bytes" 937 | version = "6.1.0" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" 940 | 941 | [[package]] 942 | name = "parking_lot" 943 | version = "0.12.1" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 946 | dependencies = [ 947 | "lock_api", 948 | "parking_lot_core", 949 | ] 950 | 951 | [[package]] 952 | name = "parking_lot_core" 953 | version = "0.9.3" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" 956 | dependencies = [ 957 | "cfg-if", 958 | "libc", 959 | "redox_syscall", 960 | "smallvec", 961 | "windows-sys", 962 | ] 963 | 964 | [[package]] 965 | name = "password-hash" 966 | version = "0.3.2" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8" 969 | dependencies = [ 970 | "base64ct", 971 | "rand_core", 972 | "subtle", 973 | ] 974 | 975 | [[package]] 976 | name = "pathdiff" 977 | version = "0.2.1" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" 980 | 981 | [[package]] 982 | name = "pbkdf2" 983 | version = "0.10.1" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" 986 | dependencies = [ 987 | "digest", 988 | "hmac", 989 | "password-hash", 990 | "sha2", 991 | ] 992 | 993 | [[package]] 994 | name = "percent-encoding" 995 | version = "2.1.0" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 998 | 999 | [[package]] 1000 | name = "pin-project" 1001 | version = "1.0.10" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" 1004 | dependencies = [ 1005 | "pin-project-internal", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "pin-project-internal" 1010 | version = "1.0.10" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" 1013 | dependencies = [ 1014 | "proc-macro2", 1015 | "quote", 1016 | "syn", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "pin-project-lite" 1021 | version = "0.2.9" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 1024 | 1025 | [[package]] 1026 | name = "pin-utils" 1027 | version = "0.1.0" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1030 | 1031 | [[package]] 1032 | name = "pkg-config" 1033 | version = "0.3.25" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" 1036 | 1037 | [[package]] 1038 | name = "ppv-lite86" 1039 | version = "0.2.16" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 1042 | 1043 | [[package]] 1044 | name = "proc-macro-error" 1045 | version = "1.0.4" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 1048 | dependencies = [ 1049 | "proc-macro-error-attr", 1050 | "proc-macro2", 1051 | "quote", 1052 | "syn", 1053 | "version_check", 1054 | ] 1055 | 1056 | [[package]] 1057 | name = "proc-macro-error-attr" 1058 | version = "1.0.4" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 1061 | dependencies = [ 1062 | "proc-macro2", 1063 | "quote", 1064 | "version_check", 1065 | ] 1066 | 1067 | [[package]] 1068 | name = "proc-macro2" 1069 | version = "1.0.39" 1070 | source = "registry+https://github.com/rust-lang/crates.io-index" 1071 | checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" 1072 | dependencies = [ 1073 | "unicode-ident", 1074 | ] 1075 | 1076 | [[package]] 1077 | name = "quote" 1078 | version = "1.0.18" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" 1081 | dependencies = [ 1082 | "proc-macro2", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "rand" 1087 | version = "0.8.5" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1090 | dependencies = [ 1091 | "libc", 1092 | "rand_chacha", 1093 | "rand_core", 1094 | ] 1095 | 1096 | [[package]] 1097 | name = "rand_chacha" 1098 | version = "0.3.1" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1101 | dependencies = [ 1102 | "ppv-lite86", 1103 | "rand_core", 1104 | ] 1105 | 1106 | [[package]] 1107 | name = "rand_core" 1108 | version = "0.6.3" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 1111 | dependencies = [ 1112 | "getrandom", 1113 | ] 1114 | 1115 | [[package]] 1116 | name = "redox_syscall" 1117 | version = "0.2.13" 1118 | source = "registry+https://github.com/rust-lang/crates.io-index" 1119 | checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" 1120 | dependencies = [ 1121 | "bitflags", 1122 | ] 1123 | 1124 | [[package]] 1125 | name = "regex" 1126 | version = "1.5.6" 1127 | source = "registry+https://github.com/rust-lang/crates.io-index" 1128 | checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" 1129 | dependencies = [ 1130 | "regex-syntax", 1131 | ] 1132 | 1133 | [[package]] 1134 | name = "regex-syntax" 1135 | version = "0.6.26" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" 1138 | 1139 | [[package]] 1140 | name = "remove_dir_all" 1141 | version = "0.5.3" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 1144 | dependencies = [ 1145 | "winapi", 1146 | ] 1147 | 1148 | [[package]] 1149 | name = "reqwest" 1150 | version = "0.11.10" 1151 | source = "registry+https://github.com/rust-lang/crates.io-index" 1152 | checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" 1153 | dependencies = [ 1154 | "base64", 1155 | "bytes", 1156 | "encoding_rs", 1157 | "futures-core", 1158 | "futures-util", 1159 | "h2", 1160 | "http", 1161 | "http-body", 1162 | "hyper", 1163 | "hyper-rustls", 1164 | "hyper-tls", 1165 | "ipnet", 1166 | "js-sys", 1167 | "lazy_static", 1168 | "log", 1169 | "mime", 1170 | "native-tls", 1171 | "percent-encoding", 1172 | "pin-project-lite", 1173 | "rustls", 1174 | "rustls-pemfile", 1175 | "serde", 1176 | "serde_json", 1177 | "serde_urlencoded", 1178 | "tokio", 1179 | "tokio-native-tls", 1180 | "tokio-rustls", 1181 | "url", 1182 | "wasm-bindgen", 1183 | "wasm-bindgen-futures", 1184 | "web-sys", 1185 | "webpki-roots", 1186 | "winreg", 1187 | ] 1188 | 1189 | [[package]] 1190 | name = "ring" 1191 | version = "0.16.20" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" 1194 | dependencies = [ 1195 | "cc", 1196 | "libc", 1197 | "once_cell", 1198 | "spin", 1199 | "untrusted", 1200 | "web-sys", 1201 | "winapi", 1202 | ] 1203 | 1204 | [[package]] 1205 | name = "rustls" 1206 | version = "0.20.6" 1207 | source = "registry+https://github.com/rust-lang/crates.io-index" 1208 | checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" 1209 | dependencies = [ 1210 | "log", 1211 | "ring", 1212 | "sct", 1213 | "webpki", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "rustls-pemfile" 1218 | version = "0.3.0" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360" 1221 | dependencies = [ 1222 | "base64", 1223 | ] 1224 | 1225 | [[package]] 1226 | name = "ryu" 1227 | version = "1.0.10" 1228 | source = "registry+https://github.com/rust-lang/crates.io-index" 1229 | checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" 1230 | 1231 | [[package]] 1232 | name = "schannel" 1233 | version = "0.1.20" 1234 | source = "registry+https://github.com/rust-lang/crates.io-index" 1235 | checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" 1236 | dependencies = [ 1237 | "lazy_static", 1238 | "windows-sys", 1239 | ] 1240 | 1241 | [[package]] 1242 | name = "scopeguard" 1243 | version = "1.1.0" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1246 | 1247 | [[package]] 1248 | name = "sct" 1249 | version = "0.7.0" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" 1252 | dependencies = [ 1253 | "ring", 1254 | "untrusted", 1255 | ] 1256 | 1257 | [[package]] 1258 | name = "security-framework" 1259 | version = "2.6.1" 1260 | source = "registry+https://github.com/rust-lang/crates.io-index" 1261 | checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" 1262 | dependencies = [ 1263 | "bitflags", 1264 | "core-foundation", 1265 | "core-foundation-sys", 1266 | "libc", 1267 | "security-framework-sys", 1268 | ] 1269 | 1270 | [[package]] 1271 | name = "security-framework-sys" 1272 | version = "2.6.1" 1273 | source = "registry+https://github.com/rust-lang/crates.io-index" 1274 | checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" 1275 | dependencies = [ 1276 | "core-foundation-sys", 1277 | "libc", 1278 | ] 1279 | 1280 | [[package]] 1281 | name = "serde" 1282 | version = "1.0.137" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" 1285 | dependencies = [ 1286 | "serde_derive", 1287 | ] 1288 | 1289 | [[package]] 1290 | name = "serde_derive" 1291 | version = "1.0.137" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" 1294 | dependencies = [ 1295 | "proc-macro2", 1296 | "quote", 1297 | "syn", 1298 | ] 1299 | 1300 | [[package]] 1301 | name = "serde_json" 1302 | version = "1.0.81" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" 1305 | dependencies = [ 1306 | "itoa", 1307 | "ryu", 1308 | "serde", 1309 | ] 1310 | 1311 | [[package]] 1312 | name = "serde_path_to_error" 1313 | version = "0.1.7" 1314 | source = "registry+https://github.com/rust-lang/crates.io-index" 1315 | checksum = "d7868ad3b8196a8a0aea99a8220b124278ee5320a55e4fde97794b6f85b1a377" 1316 | dependencies = [ 1317 | "serde", 1318 | ] 1319 | 1320 | [[package]] 1321 | name = "serde_urlencoded" 1322 | version = "0.7.1" 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" 1324 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1325 | dependencies = [ 1326 | "form_urlencoded", 1327 | "itoa", 1328 | "ryu", 1329 | "serde", 1330 | ] 1331 | 1332 | [[package]] 1333 | name = "sha1" 1334 | version = "0.10.1" 1335 | source = "registry+https://github.com/rust-lang/crates.io-index" 1336 | checksum = "c77f4e7f65455545c2153c1253d25056825e77ee2533f0e41deb65a93a34852f" 1337 | dependencies = [ 1338 | "cfg-if", 1339 | "cpufeatures", 1340 | "digest", 1341 | ] 1342 | 1343 | [[package]] 1344 | name = "sha2" 1345 | version = "0.10.2" 1346 | source = "registry+https://github.com/rust-lang/crates.io-index" 1347 | checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" 1348 | dependencies = [ 1349 | "cfg-if", 1350 | "cpufeatures", 1351 | "digest", 1352 | ] 1353 | 1354 | [[package]] 1355 | name = "signal-hook-registry" 1356 | version = "1.4.0" 1357 | source = "registry+https://github.com/rust-lang/crates.io-index" 1358 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 1359 | dependencies = [ 1360 | "libc", 1361 | ] 1362 | 1363 | [[package]] 1364 | name = "slab" 1365 | version = "0.4.6" 1366 | source = "registry+https://github.com/rust-lang/crates.io-index" 1367 | checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" 1368 | 1369 | [[package]] 1370 | name = "smallvec" 1371 | version = "1.8.0" 1372 | source = "registry+https://github.com/rust-lang/crates.io-index" 1373 | checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" 1374 | 1375 | [[package]] 1376 | name = "socket2" 1377 | version = "0.4.4" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" 1380 | dependencies = [ 1381 | "libc", 1382 | "winapi", 1383 | ] 1384 | 1385 | [[package]] 1386 | name = "spin" 1387 | version = "0.5.2" 1388 | source = "registry+https://github.com/rust-lang/crates.io-index" 1389 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 1390 | 1391 | [[package]] 1392 | name = "strsim" 1393 | version = "0.10.0" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1396 | 1397 | [[package]] 1398 | name = "subtle" 1399 | version = "2.4.1" 1400 | source = "registry+https://github.com/rust-lang/crates.io-index" 1401 | checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" 1402 | 1403 | [[package]] 1404 | name = "syn" 1405 | version = "1.0.96" 1406 | source = "registry+https://github.com/rust-lang/crates.io-index" 1407 | checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" 1408 | dependencies = [ 1409 | "proc-macro2", 1410 | "quote", 1411 | "unicode-ident", 1412 | ] 1413 | 1414 | [[package]] 1415 | name = "sync_wrapper" 1416 | version = "0.1.1" 1417 | source = "registry+https://github.com/rust-lang/crates.io-index" 1418 | checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" 1419 | 1420 | [[package]] 1421 | name = "tempfile" 1422 | version = "3.3.0" 1423 | source = "registry+https://github.com/rust-lang/crates.io-index" 1424 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 1425 | dependencies = [ 1426 | "cfg-if", 1427 | "fastrand", 1428 | "libc", 1429 | "redox_syscall", 1430 | "remove_dir_all", 1431 | "winapi", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "termcolor" 1436 | version = "1.1.3" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 1439 | dependencies = [ 1440 | "winapi-util", 1441 | ] 1442 | 1443 | [[package]] 1444 | name = "terminal_size" 1445 | version = "0.1.17" 1446 | source = "registry+https://github.com/rust-lang/crates.io-index" 1447 | checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" 1448 | dependencies = [ 1449 | "libc", 1450 | "winapi", 1451 | ] 1452 | 1453 | [[package]] 1454 | name = "textwrap" 1455 | version = "0.15.0" 1456 | source = "registry+https://github.com/rust-lang/crates.io-index" 1457 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" 1458 | 1459 | [[package]] 1460 | name = "thiserror" 1461 | version = "1.0.31" 1462 | source = "registry+https://github.com/rust-lang/crates.io-index" 1463 | checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" 1464 | dependencies = [ 1465 | "thiserror-impl", 1466 | ] 1467 | 1468 | [[package]] 1469 | name = "thiserror-impl" 1470 | version = "1.0.31" 1471 | source = "registry+https://github.com/rust-lang/crates.io-index" 1472 | checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" 1473 | dependencies = [ 1474 | "proc-macro2", 1475 | "quote", 1476 | "syn", 1477 | ] 1478 | 1479 | [[package]] 1480 | name = "time" 1481 | version = "0.3.9" 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" 1483 | checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" 1484 | dependencies = [ 1485 | "itoa", 1486 | "libc", 1487 | "num_threads", 1488 | "serde", 1489 | "time-macros", 1490 | ] 1491 | 1492 | [[package]] 1493 | name = "time-macros" 1494 | version = "0.2.4" 1495 | source = "registry+https://github.com/rust-lang/crates.io-index" 1496 | checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" 1497 | 1498 | [[package]] 1499 | name = "tinyvec" 1500 | version = "1.6.0" 1501 | source = "registry+https://github.com/rust-lang/crates.io-index" 1502 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1503 | dependencies = [ 1504 | "tinyvec_macros", 1505 | ] 1506 | 1507 | [[package]] 1508 | name = "tinyvec_macros" 1509 | version = "0.1.0" 1510 | source = "registry+https://github.com/rust-lang/crates.io-index" 1511 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1512 | 1513 | [[package]] 1514 | name = "tokio" 1515 | version = "1.19.2" 1516 | source = "registry+https://github.com/rust-lang/crates.io-index" 1517 | checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" 1518 | dependencies = [ 1519 | "bytes", 1520 | "libc", 1521 | "memchr", 1522 | "mio", 1523 | "num_cpus", 1524 | "once_cell", 1525 | "parking_lot", 1526 | "pin-project-lite", 1527 | "signal-hook-registry", 1528 | "socket2", 1529 | "tokio-macros", 1530 | "winapi", 1531 | ] 1532 | 1533 | [[package]] 1534 | name = "tokio-macros" 1535 | version = "1.8.0" 1536 | source = "registry+https://github.com/rust-lang/crates.io-index" 1537 | checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" 1538 | dependencies = [ 1539 | "proc-macro2", 1540 | "quote", 1541 | "syn", 1542 | ] 1543 | 1544 | [[package]] 1545 | name = "tokio-native-tls" 1546 | version = "0.3.0" 1547 | source = "registry+https://github.com/rust-lang/crates.io-index" 1548 | checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" 1549 | dependencies = [ 1550 | "native-tls", 1551 | "tokio", 1552 | ] 1553 | 1554 | [[package]] 1555 | name = "tokio-rustls" 1556 | version = "0.23.4" 1557 | source = "registry+https://github.com/rust-lang/crates.io-index" 1558 | checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" 1559 | dependencies = [ 1560 | "rustls", 1561 | "tokio", 1562 | "webpki", 1563 | ] 1564 | 1565 | [[package]] 1566 | name = "tokio-util" 1567 | version = "0.7.3" 1568 | source = "registry+https://github.com/rust-lang/crates.io-index" 1569 | checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" 1570 | dependencies = [ 1571 | "bytes", 1572 | "futures-core", 1573 | "futures-sink", 1574 | "pin-project-lite", 1575 | "tokio", 1576 | "tracing", 1577 | ] 1578 | 1579 | [[package]] 1580 | name = "tower" 1581 | version = "0.4.12" 1582 | source = "registry+https://github.com/rust-lang/crates.io-index" 1583 | checksum = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e" 1584 | dependencies = [ 1585 | "futures-core", 1586 | "futures-util", 1587 | "pin-project", 1588 | "pin-project-lite", 1589 | "tokio", 1590 | "tokio-util", 1591 | "tower-layer", 1592 | "tower-service", 1593 | "tracing", 1594 | ] 1595 | 1596 | [[package]] 1597 | name = "tower-http" 1598 | version = "0.3.4" 1599 | source = "registry+https://github.com/rust-lang/crates.io-index" 1600 | checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba" 1601 | dependencies = [ 1602 | "bitflags", 1603 | "bytes", 1604 | "futures-core", 1605 | "futures-util", 1606 | "http", 1607 | "http-body", 1608 | "http-range-header", 1609 | "pin-project-lite", 1610 | "tower", 1611 | "tower-layer", 1612 | "tower-service", 1613 | ] 1614 | 1615 | [[package]] 1616 | name = "tower-layer" 1617 | version = "0.3.1" 1618 | source = "registry+https://github.com/rust-lang/crates.io-index" 1619 | checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" 1620 | 1621 | [[package]] 1622 | name = "tower-service" 1623 | version = "0.3.1" 1624 | source = "registry+https://github.com/rust-lang/crates.io-index" 1625 | checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" 1626 | 1627 | [[package]] 1628 | name = "tracing" 1629 | version = "0.1.35" 1630 | source = "registry+https://github.com/rust-lang/crates.io-index" 1631 | checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" 1632 | dependencies = [ 1633 | "cfg-if", 1634 | "log", 1635 | "pin-project-lite", 1636 | "tracing-core", 1637 | ] 1638 | 1639 | [[package]] 1640 | name = "tracing-core" 1641 | version = "0.1.27" 1642 | source = "registry+https://github.com/rust-lang/crates.io-index" 1643 | checksum = "7709595b8878a4965ce5e87ebf880a7d39c9afc6837721b21a5a816a8117d921" 1644 | dependencies = [ 1645 | "once_cell", 1646 | ] 1647 | 1648 | [[package]] 1649 | name = "try-lock" 1650 | version = "0.2.3" 1651 | source = "registry+https://github.com/rust-lang/crates.io-index" 1652 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 1653 | 1654 | [[package]] 1655 | name = "typenum" 1656 | version = "1.15.0" 1657 | source = "registry+https://github.com/rust-lang/crates.io-index" 1658 | checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" 1659 | 1660 | [[package]] 1661 | name = "unicode-bidi" 1662 | version = "0.3.8" 1663 | source = "registry+https://github.com/rust-lang/crates.io-index" 1664 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 1665 | 1666 | [[package]] 1667 | name = "unicode-ident" 1668 | version = "1.0.0" 1669 | source = "registry+https://github.com/rust-lang/crates.io-index" 1670 | checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" 1671 | 1672 | [[package]] 1673 | name = "unicode-normalization" 1674 | version = "0.1.19" 1675 | source = "registry+https://github.com/rust-lang/crates.io-index" 1676 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" 1677 | dependencies = [ 1678 | "tinyvec", 1679 | ] 1680 | 1681 | [[package]] 1682 | name = "untrusted" 1683 | version = "0.7.1" 1684 | source = "registry+https://github.com/rust-lang/crates.io-index" 1685 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 1686 | 1687 | [[package]] 1688 | name = "url" 1689 | version = "2.2.2" 1690 | source = "registry+https://github.com/rust-lang/crates.io-index" 1691 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 1692 | dependencies = [ 1693 | "form_urlencoded", 1694 | "idna", 1695 | "matches", 1696 | "percent-encoding", 1697 | "serde", 1698 | ] 1699 | 1700 | [[package]] 1701 | name = "vcpkg" 1702 | version = "0.2.15" 1703 | source = "registry+https://github.com/rust-lang/crates.io-index" 1704 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1705 | 1706 | [[package]] 1707 | name = "version_check" 1708 | version = "0.9.4" 1709 | source = "registry+https://github.com/rust-lang/crates.io-index" 1710 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1711 | 1712 | [[package]] 1713 | name = "want" 1714 | version = "0.3.0" 1715 | source = "registry+https://github.com/rust-lang/crates.io-index" 1716 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1717 | dependencies = [ 1718 | "log", 1719 | "try-lock", 1720 | ] 1721 | 1722 | [[package]] 1723 | name = "wasi" 1724 | version = "0.10.2+wasi-snapshot-preview1" 1725 | source = "registry+https://github.com/rust-lang/crates.io-index" 1726 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 1727 | 1728 | [[package]] 1729 | name = "wasi" 1730 | version = "0.11.0+wasi-snapshot-preview1" 1731 | source = "registry+https://github.com/rust-lang/crates.io-index" 1732 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1733 | 1734 | [[package]] 1735 | name = "wasm-bindgen" 1736 | version = "0.2.80" 1737 | source = "registry+https://github.com/rust-lang/crates.io-index" 1738 | checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" 1739 | dependencies = [ 1740 | "cfg-if", 1741 | "wasm-bindgen-macro", 1742 | ] 1743 | 1744 | [[package]] 1745 | name = "wasm-bindgen-backend" 1746 | version = "0.2.80" 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" 1748 | checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" 1749 | dependencies = [ 1750 | "bumpalo", 1751 | "lazy_static", 1752 | "log", 1753 | "proc-macro2", 1754 | "quote", 1755 | "syn", 1756 | "wasm-bindgen-shared", 1757 | ] 1758 | 1759 | [[package]] 1760 | name = "wasm-bindgen-futures" 1761 | version = "0.4.30" 1762 | source = "registry+https://github.com/rust-lang/crates.io-index" 1763 | checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" 1764 | dependencies = [ 1765 | "cfg-if", 1766 | "js-sys", 1767 | "wasm-bindgen", 1768 | "web-sys", 1769 | ] 1770 | 1771 | [[package]] 1772 | name = "wasm-bindgen-macro" 1773 | version = "0.2.80" 1774 | source = "registry+https://github.com/rust-lang/crates.io-index" 1775 | checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" 1776 | dependencies = [ 1777 | "quote", 1778 | "wasm-bindgen-macro-support", 1779 | ] 1780 | 1781 | [[package]] 1782 | name = "wasm-bindgen-macro-support" 1783 | version = "0.2.80" 1784 | source = "registry+https://github.com/rust-lang/crates.io-index" 1785 | checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" 1786 | dependencies = [ 1787 | "proc-macro2", 1788 | "quote", 1789 | "syn", 1790 | "wasm-bindgen-backend", 1791 | "wasm-bindgen-shared", 1792 | ] 1793 | 1794 | [[package]] 1795 | name = "wasm-bindgen-shared" 1796 | version = "0.2.80" 1797 | source = "registry+https://github.com/rust-lang/crates.io-index" 1798 | checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" 1799 | 1800 | [[package]] 1801 | name = "web-sys" 1802 | version = "0.3.57" 1803 | source = "registry+https://github.com/rust-lang/crates.io-index" 1804 | checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" 1805 | dependencies = [ 1806 | "js-sys", 1807 | "wasm-bindgen", 1808 | ] 1809 | 1810 | [[package]] 1811 | name = "webpki" 1812 | version = "0.22.0" 1813 | source = "registry+https://github.com/rust-lang/crates.io-index" 1814 | checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" 1815 | dependencies = [ 1816 | "ring", 1817 | "untrusted", 1818 | ] 1819 | 1820 | [[package]] 1821 | name = "webpki-roots" 1822 | version = "0.22.3" 1823 | source = "registry+https://github.com/rust-lang/crates.io-index" 1824 | checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" 1825 | dependencies = [ 1826 | "webpki", 1827 | ] 1828 | 1829 | [[package]] 1830 | name = "winapi" 1831 | version = "0.3.9" 1832 | source = "registry+https://github.com/rust-lang/crates.io-index" 1833 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1834 | dependencies = [ 1835 | "winapi-i686-pc-windows-gnu", 1836 | "winapi-x86_64-pc-windows-gnu", 1837 | ] 1838 | 1839 | [[package]] 1840 | name = "winapi-i686-pc-windows-gnu" 1841 | version = "0.4.0" 1842 | source = "registry+https://github.com/rust-lang/crates.io-index" 1843 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1844 | 1845 | [[package]] 1846 | name = "winapi-util" 1847 | version = "0.1.5" 1848 | source = "registry+https://github.com/rust-lang/crates.io-index" 1849 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1850 | dependencies = [ 1851 | "winapi", 1852 | ] 1853 | 1854 | [[package]] 1855 | name = "winapi-x86_64-pc-windows-gnu" 1856 | version = "0.4.0" 1857 | source = "registry+https://github.com/rust-lang/crates.io-index" 1858 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1859 | 1860 | [[package]] 1861 | name = "windows-sys" 1862 | version = "0.36.1" 1863 | source = "registry+https://github.com/rust-lang/crates.io-index" 1864 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 1865 | dependencies = [ 1866 | "windows_aarch64_msvc", 1867 | "windows_i686_gnu", 1868 | "windows_i686_msvc", 1869 | "windows_x86_64_gnu", 1870 | "windows_x86_64_msvc", 1871 | ] 1872 | 1873 | [[package]] 1874 | name = "windows_aarch64_msvc" 1875 | version = "0.36.1" 1876 | source = "registry+https://github.com/rust-lang/crates.io-index" 1877 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 1878 | 1879 | [[package]] 1880 | name = "windows_i686_gnu" 1881 | version = "0.36.1" 1882 | source = "registry+https://github.com/rust-lang/crates.io-index" 1883 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 1884 | 1885 | [[package]] 1886 | name = "windows_i686_msvc" 1887 | version = "0.36.1" 1888 | source = "registry+https://github.com/rust-lang/crates.io-index" 1889 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 1890 | 1891 | [[package]] 1892 | name = "windows_x86_64_gnu" 1893 | version = "0.36.1" 1894 | source = "registry+https://github.com/rust-lang/crates.io-index" 1895 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 1896 | 1897 | [[package]] 1898 | name = "windows_x86_64_msvc" 1899 | version = "0.36.1" 1900 | source = "registry+https://github.com/rust-lang/crates.io-index" 1901 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 1902 | 1903 | [[package]] 1904 | name = "winreg" 1905 | version = "0.10.1" 1906 | source = "registry+https://github.com/rust-lang/crates.io-index" 1907 | checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" 1908 | dependencies = [ 1909 | "winapi", 1910 | ] 1911 | 1912 | [[package]] 1913 | name = "zip" 1914 | version = "0.6.2" 1915 | source = "registry+https://github.com/rust-lang/crates.io-index" 1916 | checksum = "bf225bcf73bb52cbb496e70475c7bd7a3f769df699c0020f6c7bd9a96dcf0b8d" 1917 | dependencies = [ 1918 | "aes", 1919 | "byteorder", 1920 | "bzip2", 1921 | "constant_time_eq", 1922 | "crc32fast", 1923 | "crossbeam-utils", 1924 | "flate2", 1925 | "hmac", 1926 | "pbkdf2", 1927 | "sha1", 1928 | "time", 1929 | "zstd", 1930 | ] 1931 | 1932 | [[package]] 1933 | name = "zstd" 1934 | version = "0.10.2+zstd.1.5.2" 1935 | source = "registry+https://github.com/rust-lang/crates.io-index" 1936 | checksum = "5f4a6bd64f22b5e3e94b4e238669ff9f10815c27a5180108b849d24174a83847" 1937 | dependencies = [ 1938 | "zstd-safe", 1939 | ] 1940 | 1941 | [[package]] 1942 | name = "zstd-safe" 1943 | version = "4.1.6+zstd.1.5.2" 1944 | source = "registry+https://github.com/rust-lang/crates.io-index" 1945 | checksum = "94b61c51bb270702d6167b8ce67340d2754b088d0c091b06e593aa772c3ee9bb" 1946 | dependencies = [ 1947 | "libc", 1948 | "zstd-sys", 1949 | ] 1950 | 1951 | [[package]] 1952 | name = "zstd-sys" 1953 | version = "1.6.3+zstd.1.5.2" 1954 | source = "registry+https://github.com/rust-lang/crates.io-index" 1955 | checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8" 1956 | dependencies = [ 1957 | "cc", 1958 | "libc", 1959 | ] 1960 | --------------------------------------------------------------------------------