1use anyhow::{Error, Result};
2use reqwest::Client;
3use scraper::{Html, Selector};
4use std::collections::HashMap;
5use std::fmt::Formatter;
6use std::time::Duration;
7use url::Url;
8
9const START_PATTERN: &str = "location.href='";
10const END_PATTERN: &str = "';</script>";
11
12#[derive(Debug, Clone)]
13pub enum ParseError {
14 NoLoginForm,
15 NoFormAction,
16 InvalidFormInputField,
17 InvalidRedirectData,
18 AuthorizationFailed,
19}
20
21impl std::fmt::Display for ParseError {
22 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
23 match self {
24 ParseError::NoLoginForm => {
25 write!(f, "NoLoginForm")
26 }
27 ParseError::NoFormAction => {
28 write!(f, "NoFormAction")
29 }
30 ParseError::InvalidFormInputField => {
31 write!(f, "InvalidFormInputField")
32 }
33 ParseError::InvalidRedirectData => {
34 write!(f, "InvalidRedirectData: expected data not found on page")
35 }
36 ParseError::AuthorizationFailed => {
37 write!(f, "AuthorizationFailed: invalid authorization data")
38 }
39 }?;
40 Ok(())
41 }
42}
43
44impl std::error::Error for ParseError {}
45
46#[derive(Debug, Clone)]
47pub struct AccessToken {
48 access_token: String,
49 expires_in: Duration,
50 user_id: String,
51}
52
53impl AccessToken {
54 pub fn access_token(&self) -> &str {
55 &self.access_token
56 }
57 pub fn expires_in(&self) -> Duration {
58 self.expires_in
59 }
60 pub fn user_id(&self) -> &str {
61 &self.user_id
62 }
63}
64
65#[derive(Debug, Clone)]
66pub struct Authorizer {
67 client: Client,
68}
69
70#[derive(Debug, Clone)]
71pub struct AuthorizerBuilder {
72 client: Option<Client>,
73}
74
75impl AuthorizerBuilder {
76 pub fn new() -> Self {
77 Self { client: None }
78 }
79
80 pub fn with_client(mut self, client: Client) -> Self {
81 self.client = Some(client);
82 self
83 }
84
85 pub fn build(self) -> Result<Authorizer> {
86 let client = self
87 .client
88 .unwrap_or(reqwest::Client::builder().cookie_store(true).build()?);
89 Ok(Authorizer { client })
90 }
91}
92
93impl Default for AuthorizerBuilder {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99impl Authorizer {
100 pub fn builder() -> AuthorizerBuilder {
101 AuthorizerBuilder::new()
102 }
103
104 pub async fn get_token(
105 &self,
106 api_id: &str,
107 email_or_phone: &str,
108 password: &str,
109 ) -> Result<AccessToken> {
110 let initial_resp = self
111 .client
112 .get(format!(
113 "https://oauth.vk.com/oauth/authorize?client_id={}&scope=0&response_type=token",
114 api_id
115 ))
116 .send()
117 .await?
118 .text()
119 .await?;
120 let doc = Html::parse_document(initial_resp.as_str());
121 let form = doc
122 .select(&Selector::parse("form").unwrap())
123 .next()
124 .ok_or(ParseError::NoLoginForm)?;
125 let url = form
126 .value()
127 .attr("action")
128 .ok_or(ParseError::NoFormAction)?;
129 let mut data = HashMap::new();
130 data.insert("email", email_or_phone);
131 data.insert("pass", password);
132 data.insert("expire", "0");
133 for node in form.children() {
134 let val = node.value();
135 if !val.is_element() {
136 continue;
137 }
138 let inp = node.value().as_element().unwrap();
139 if inp.name() != "input" {
140 continue;
141 }
142 if inp.attr("type").unwrap() == "hidden" {
143 data.insert(
144 inp.attr("name").ok_or(ParseError::InvalidFormInputField)?,
145 inp.attr("value").ok_or(ParseError::InvalidFormInputField)?,
146 );
147 }
148 }
149 let resp = self
150 .client
151 .post(url)
152 .form(&data)
153 .send()
154 .await?
155 .text()
156 .await?;
157 get_token_from_page(resp.as_str()).map_err(|e| match e.downcast_ref::<ParseError>() {
158 Some(parse_err) => match parse_err {
159 ParseError::NoLoginForm => e,
160 ParseError::NoFormAction => e,
161 ParseError::InvalidFormInputField => e,
162 ParseError::InvalidRedirectData => {
163 if doc
165 .select(
166 &Selector::parse(r#"form[action*="https://login.vk.com"]"#).unwrap(),
167 )
168 .next()
169 .is_some()
170 {
171 Error::from(ParseError::AuthorizationFailed)
172 } else {
173 e
174 }
175 }
176 ParseError::AuthorizationFailed => e,
177 },
178 None => e,
179 })
180 }
181}
182
183fn get_token_from_page(resp: &str) -> Result<AccessToken> {
184 let sfound = resp
185 .find(START_PATTERN)
186 .ok_or(ParseError::InvalidRedirectData)?;
187 let efound = resp
188 .rfind(END_PATTERN)
189 .ok_or(ParseError::InvalidRedirectData)?;
190 let redirect_url = resp[sfound + START_PATTERN.len()..efound].into();
191 let query: HashMap<_, _> = form_urlencoded::parse(
192 Url::parse(redirect_url)?
193 .fragment()
194 .ok_or(ParseError::InvalidRedirectData)?
195 .as_bytes(),
196 )
197 .into_owned()
198 .collect();
199 Ok(AccessToken {
200 access_token: query
201 .get("access_token")
202 .ok_or(ParseError::InvalidRedirectData)?
203 .into(),
204 expires_in: Duration::from_secs(
205 query
206 .get("expires_in")
207 .ok_or(ParseError::InvalidRedirectData)?
208 .parse()?,
209 ),
210 user_id: query
211 .get("user_id")
212 .ok_or(ParseError::InvalidRedirectData)?
213 .into(),
214 })
215}