1use crate::{post_list::Post, thread_list::Thread, Error};
2use reqwest_cookie_store::{CookieStore, CookieStoreMutex};
3use std::{
4 io::{BufRead, Write},
5 sync::Arc,
6};
7use url::Url;
8
9pub struct Client {
11 base: Url,
12 client: reqwest::Client,
13 cookie_store: Arc<CookieStoreMutex>,
14}
15
16pub enum User<'a> {
18 CurrentUser,
20
21 UserID(&'a str),
23
24 Username(&'a str),
26}
27
28pub enum ThreadPage {
30 First,
32
33 Last,
35
36 New,
38
39 Page(usize),
42}
43
44#[derive(Debug, serde::Serialize, serde::Deserialize)]
46pub struct Profile {
47 pub userid: i64,
48 pub username: String,
49 pub homepage: String,
50 pub icq: String,
51 pub aim: String,
52 pub yahoo: String,
53 pub gender: String,
54 pub usertitle: String,
55 pub joindate: i64,
56 pub lastpost: i64,
57 pub posts: i64,
58 pub receivepm: i64,
59 pub postsperday: f64,
60 pub role: String,
61 pub biography: String,
62 pub location: String,
63 pub interests: String,
64 pub occupation: String,
65 pub picture: String,
66 pub avpath: String,
67}
68
69impl Client {
70 pub fn new() -> Result<Client, Error> {
73 let cookie_store = Arc::new(CookieStoreMutex::new(CookieStore::new(None)));
74 Ok(Client {
75 base: Url::parse("https://forums.somethingawful.com")?,
76 client: reqwest::Client::builder()
77 .cookie_provider(cookie_store.clone())
78 .build()?,
79 cookie_store,
80 })
81 }
82
83 pub async fn login(&self, username: &str, password: &str) -> Result<(), Error> {
86 let response = self
87 .client
88 .post(self.base.join("account.php?json=1")?)
89 .form(&[
90 ("action", "login"),
91 ("username", username),
92 ("password", password),
93 ("next", "/index.php?json=1"),
94 ])
95 .send()
96 .await?;
97
98 if response.error_for_status().is_err() {
99 Err(Error::LoginError)
100 } else {
101 Ok(())
102 }
103 }
104
105 pub async fn fetch_profile<'a>(&self, user: User<'a>) -> Result<Option<Profile>, Error> {
107 let query = match user {
108 User::CurrentUser => vec![("action", "getinfo"), ("json", "1")],
109 User::UserID(userid) => vec![("action", "getinfo"), ("userid", userid), ("json", "1")],
110 User::Username(username) => {
111 vec![("action", "getinfo"), ("username", username), ("json", "1")]
112 }
113 };
114 let response = self
115 .client
116 .get(self.base.join("member.php")?)
117 .query(&query)
118 .send()
119 .await?;
120
121 match response.json().await {
124 Ok(res) => Ok(res),
125 Err(err) => {
126 if err.is_decode() {
127 Ok(None)
128 } else {
129 Err(err.into())
130 }
131 }
132 }
133 }
134
135 pub async fn fetch_posts(
137 &self,
138 thread_id: &str,
139 index: ThreadPage,
140 ) -> Result<Vec<Post>, Error> {
141 let mut _page_string = None;
142 let query = match index {
143 ThreadPage::First => {
144 vec![("threadid", thread_id), ("perpage", "40")]
145 }
146 ThreadPage::Last => {
147 vec![
148 ("threadid", thread_id),
149 ("perpage", "40"),
150 ("goto", "lastpost"),
151 ]
152 }
153 ThreadPage::New => {
154 vec![
155 ("threadid", thread_id),
156 ("perpage", "40"),
157 ("goto", "newpost"),
158 ]
159 }
160 ThreadPage::Page(page) => {
161 _page_string = Some(format!("{page}"));
162 vec![
163 ("threadid", thread_id),
164 ("perpage", "40"),
165 ("pagenumber", _page_string.as_ref().unwrap()),
166 ]
167 }
168 };
169 let response = self
170 .client
171 .get(self.base.join("showthread.php")?)
172 .query(&query)
173 .send()
174 .await?
175 .text()
176 .await?;
177
178 Post::parse_list(&response)
179 }
180
181 pub async fn fetch_bookmarked_threads(&self) -> Result<Vec<Thread>, Error> {
183 let mut bookmarked_threads = Vec::new();
184 let mut page = 1;
185 loop {
186 let response = self
187 .client
188 .get(self.base.join("bookmarkthreads.php")?)
189 .query(&[
190 ("action", "view"),
191 ("perpage", "40"),
192 ("pagenumber", &format!("{page}")),
193 ])
194 .send()
195 .await?
196 .text()
197 .await?;
198
199 let mut threads = Thread::parse_list(&response)?;
200 let fetch_next = threads.len() == 40;
201 bookmarked_threads.append(&mut threads);
202 if fetch_next {
203 page += 1;
204 } else {
205 break;
206 }
207 }
208 Ok(bookmarked_threads)
209 }
210
211 pub fn save_credentials<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
214 let store = self.cookie_store.lock().expect("BUG: lock failed");
215 store.save_json(writer).map_err(Error::CookieIOError)?;
216 Ok(())
217 }
218
219 pub fn load_credentials<R: BufRead>(&self, reader: R) -> Result<(), Error> {
222 let loaded = CookieStore::load_json(reader).map_err(Error::CookieIOError)?;
223 let mut store = self.cookie_store.lock().expect("BUG: lock failed");
224 for cookie in loaded.iter_unexpired() {
225 store.insert(cookie.clone(), &self.base)?;
226 }
227 Ok(())
228 }
229}