1use crate::error::FunPayError;
2use crate::events::Event;
3use crate::utils::random_tag;
4use log::info;
5use scraper::{Html, Selector};
6use serde_json::Value;
7use std::collections::HashMap;
8use std::time::Duration;
9use tokio::sync::broadcast;
10
11#[derive(Debug, Clone)]
12pub struct ChatShortcut {
13 pub id: i64,
14 pub name: String,
15 pub last_message_text: Option<String>,
16 pub node_msg_id: i64,
17 pub user_msg_id: i64,
18 pub unread: bool,
19}
20
21#[derive(Debug, Clone)]
22pub struct Message {
23 pub id: i64,
24 pub chat_id: String,
25 pub chat_name: Option<String>,
26 pub text: Option<String>,
27 pub interlocutor_id: Option<i64>,
28 pub author_id: i64,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum OrderStatus {
33 Paid,
34 Closed,
35 Refunded,
36}
37
38#[derive(Debug, Clone)]
39pub struct OrderShortcut {
40 pub id: String,
41 pub status: OrderStatus,
42}
43
44#[derive(Debug, Clone)]
45struct AppData {
46 user_id: i64,
47 csrf_token: String,
48}
49
50#[derive(Debug)]
51pub struct FunPayAccount {
52 client: reqwest::Client,
53 pub golden_key: String,
54 user_agent: String,
55 pub id: Option<i64>,
56 pub username: Option<String>,
57 pub csrf_token: Option<String>,
58 last_msg_event_tag: String,
59 last_order_event_tag: String,
60 last_messages: HashMap<i64, (i64, i64, Option<String>)>,
61 last_messages_ids: HashMap<i64, i64>,
62 saved_orders: HashMap<String, OrderShortcut>,
63 pub events_tx: broadcast::Sender<Event>,
64}
65
66impl FunPayAccount {
67 pub fn new(golden_key: String) -> Self {
68 let client = reqwest::Client::builder()
69 .redirect(reqwest::redirect::Policy::limited(10))
70 .build()
71 .unwrap();
72 let (tx, _rx) = broadcast::channel(512);
73 Self {
74 client,
75 golden_key,
76 user_agent: String::from("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"),
77 id: None,
78 username: None,
79 csrf_token: None,
80 last_msg_event_tag: random_tag(),
81 last_order_event_tag: random_tag(),
82 last_messages: HashMap::new(),
83 last_messages_ids: HashMap::new(),
84 saved_orders: HashMap::new(),
85 events_tx: tx,
86 }
87 }
88
89 pub fn subscribe(&self) -> broadcast::Receiver<Event> {
90 self.events_tx.subscribe()
91 }
92
93 async fn get(&mut self) -> Result<(), FunPayError> {
94 let url = "https://funpay.com/";
95 let resp = self
96 .client
97 .get(url)
98 .header(
99 reqwest::header::COOKIE,
100 format!("golden_key={}; cookie_prefs=1", self.golden_key),
101 )
102 .header(reqwest::header::USER_AGENT, &self.user_agent)
103 .send()
104 .await?;
105 if resp.status() == reqwest::StatusCode::FORBIDDEN {
106 return Err(FunPayError::Unauthorized);
107 }
108 if !resp.status().is_success() {
109 let status = resp.status();
110 let body = resp.text().await.unwrap_or_default();
111 return Err(FunPayError::RequestFailed {
112 status,
113 body,
114 url: url.to_string(),
115 });
116 }
117 let body = resp.text().await?;
118 let html = Html::parse_document(&body);
119 let sel_body = Selector::parse("body").unwrap();
120 let mut app_data: Option<AppData> = None;
121 if let Some(b) = html.select(&sel_body).next() {
122 if let Some(attr) = b.value().attr("data-app-data") {
123 let v: Value =
124 serde_json::from_str(attr).map_err(|e| FunPayError::Parse(e.to_string()))?;
125 let user_id = v
126 .get("userId")
127 .and_then(|x| x.as_i64())
128 .ok_or_else(|| FunPayError::Parse(String::from("missing userId")))?;
129 let csrf = v
130 .get("csrf-token")
131 .and_then(|x| x.as_str())
132 .ok_or_else(|| FunPayError::Parse(String::from("missing csrf-token")))?;
133 app_data = Some(AppData {
134 user_id,
135 csrf_token: csrf.to_string(),
136 });
137 }
138 }
139 let sel_uname = Selector::parse("div.user-link-name").unwrap();
140 let username = html
141 .select(&sel_uname)
142 .next()
143 .map(|n| n.text().collect::<String>());
144 if username.is_none() {
145 return Err(FunPayError::Unauthorized);
146 }
147 let app = app_data.ok_or_else(|| FunPayError::Parse(String::from("missing app data")))?;
148 self.id = Some(app.user_id);
149 self.csrf_token = Some(app.csrf_token);
150 self.username = username;
151 Ok(())
152 }
153
154 async fn post_runner(&self, objects_json: String) -> Result<Value, FunPayError> {
155 let csrf = self
156 .csrf_token
157 .as_ref()
158 .ok_or(FunPayError::AccountNotInitiated)?
159 .to_string();
160 let url = "https://funpay.com/runner/";
161 let payload = format!(
162 "objects={}&request=false&csrf_token={}",
163 urlencoding::encode(&objects_json),
164 urlencoding::encode(&csrf)
165 );
166 let resp = self
167 .client
168 .post(url)
169 .header(
170 reqwest::header::CONTENT_TYPE,
171 "application/x-www-form-urlencoded; charset=UTF-8",
172 )
173 .header("x-requested-with", "XMLHttpRequest")
174 .header(reqwest::header::ACCEPT, "*/*")
175 .header(
176 reqwest::header::COOKIE,
177 format!("golden_key={}; cookie_prefs=1", self.golden_key),
178 )
179 .header(reqwest::header::USER_AGENT, &self.user_agent)
180 .body(payload)
181 .send()
182 .await?;
183 if resp.status() == reqwest::StatusCode::FORBIDDEN {
184 return Err(FunPayError::Unauthorized);
185 }
186 if !resp.status().is_success() {
187 let status = resp.status();
188 let body = resp.text().await.unwrap_or_default();
189 return Err(FunPayError::RequestFailed {
190 status,
191 body,
192 url: url.to_string(),
193 });
194 }
195 let v: Value = resp.json().await?;
196 Ok(v)
197 }
198
199 fn parse_chat_bookmarks(&mut self, html: &str) -> Vec<ChatShortcut> {
200 let doc = Html::parse_fragment(html);
201 let sel_chat = Selector::parse("a.contact-item").unwrap();
202 let sel_msg = Selector::parse("div.contact-item-message").unwrap();
203 let sel_name = Selector::parse("div.media-user-name").unwrap();
204 let mut out = Vec::new();
205 for el in doc.select(&sel_chat) {
206 let id_attr = el.value().attr("data-id").unwrap_or("0");
207 let id = id_attr.parse::<i64>().unwrap_or(0);
208 let node_msg_id = el
209 .value()
210 .attr("data-node-msg")
211 .unwrap_or("0")
212 .parse::<i64>()
213 .unwrap_or(0);
214 let user_msg_id = el
215 .value()
216 .attr("data-user-msg")
217 .unwrap_or("0")
218 .parse::<i64>()
219 .unwrap_or(0);
220 let unread = el.value().classes().any(|c| c == "unread");
221 let last_message_text = el
222 .select(&sel_msg)
223 .next()
224 .map(|n| n.text().collect::<String>());
225 let name = el
226 .select(&sel_name)
227 .next()
228 .map(|n| n.text().collect::<String>())
229 .unwrap_or_default();
230 out.push(ChatShortcut {
231 id,
232 name,
233 last_message_text,
234 node_msg_id,
235 user_msg_id,
236 unread,
237 });
238 }
239 out
240 }
241
242 fn parse_events_from_updates(
243 &mut self,
244 updates: &Value,
245 first: bool,
246 ) -> (Vec<Event>, Vec<(i64, Option<String>)>) {
247 let mut events = Vec::new();
248 let mut changed_chats: Vec<ChatShortcut> = Vec::new();
249 let objects = updates
250 .get("objects")
251 .and_then(|x| x.as_array())
252 .cloned()
253 .unwrap_or_default();
254 for obj in objects {
255 let typ = obj.get("type").and_then(|x| x.as_str()).unwrap_or("");
256 if typ == "chat_bookmarks" {
257 if let Some(tag) = obj.get("tag").and_then(|x| x.as_str()) {
258 self.last_msg_event_tag = tag.to_string();
259 }
260 let html = obj
261 .get("data")
262 .and_then(|x| x.get("html"))
263 .and_then(|x| x.as_str())
264 .unwrap_or("");
265 if html.is_empty() {
266 continue;
267 }
268 let chats = self.parse_chat_bookmarks(html);
269 if first {
270 for ch in chats {
271 events.push(Event::InitialChat { chat: ch });
272 }
273 } else {
274 if !chats.is_empty() {
275 events.push(Event::ChatsListChanged);
276 }
277 for ch in chats {
278 let prev = self
279 .last_messages
280 .get(&ch.id)
281 .cloned()
282 .unwrap_or((-1, -1, None));
283 if ch.node_msg_id > prev.0 {
284 events.push(Event::LastChatMessageChanged { chat: ch.clone() });
285 changed_chats.push(ch.clone());
286 }
287 self.last_messages.insert(
288 ch.id,
289 (ch.node_msg_id, ch.user_msg_id, ch.last_message_text.clone()),
290 );
291 }
292 }
293 } else if typ == "orders_counters" {
294 if let Some(tag) = obj.get("tag").and_then(|x| x.as_str()) {
295 self.last_order_event_tag = tag.to_string();
296 }
297 let purchases = obj
298 .get("data")
299 .and_then(|x| x.get("buyer"))
300 .and_then(|x| x.as_i64())
301 .unwrap_or(0) as i32;
302 let sales = obj
303 .get("data")
304 .and_then(|x| x.get("seller"))
305 .and_then(|x| x.as_i64())
306 .unwrap_or(0) as i32;
307 events.push(Event::OrdersListChanged { purchases, sales });
308 }
309 }
310 let chats_data: Vec<(i64, Option<String>)> = if first {
311 Vec::new()
312 } else {
313 changed_chats
314 .into_iter()
315 .map(|c| (c.id, Some(c.name)))
316 .collect()
317 };
318 (events, chats_data)
319 }
320
321 pub async fn start_polling_loop(&mut self) -> std::io::Result<()> {
322 self.get()
323 .await
324 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
325 info!(target: "funpay_client", "Logged in as {}", self.username.clone().unwrap_or_default());
326 let mut first = true;
327 loop {
328 let orders = serde_json::json!({
329 "type": "orders_counters",
330 "id": self.id.unwrap_or_default(),
331 "tag": self.last_order_event_tag,
332 "data": false
333 });
334 let chats = serde_json::json!({
335 "type": "chat_bookmarks",
336 "id": self.id.unwrap_or_default(),
337 "tag": self.last_msg_event_tag,
338 "data": false
339 });
340 let objects_json = serde_json::to_string(&serde_json::json!([orders, chats])).unwrap();
341 let updates = self
342 .post_runner(objects_json)
343 .await
344 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
345 let (evs, changed_chats) = self.parse_events_from_updates(&updates, first);
346 for ev in evs {
347 let _ = self.events_tx.send(ev);
348 }
349 if !first && !changed_chats.is_empty() {
350 match self.fetch_chats_histories(&changed_chats).await {
351 Ok(mut histories) => {
352 for (cid, mut msgs) in histories.drain() {
353 if let Some(last_id) = self.last_messages_ids.get(&cid).copied() {
354 msgs.retain(|m| m.id > last_id);
355 }
356 if let Some(max_id) = msgs.iter().map(|m| m.id).max() {
357 self.last_messages_ids.insert(cid, max_id);
358 }
359 for m in msgs {
360 let _ = self.events_tx.send(Event::NewMessage { message: m });
361 }
362 }
363 }
364 Err(e) => {
365 log::error!(target: "funpay_client", "Failed to fetch chat histories: {}", e);
366 }
367 }
368 }
369 match self.fetch_sales_list().await {
370 Ok(list) => {
371 let mut new_map: HashMap<String, OrderShortcut> = HashMap::new();
372 for o in list.into_iter() {
373 new_map.insert(o.id.clone(), o);
374 }
375 if self.saved_orders.is_empty() {
376 for order in new_map.values() {
377 let _ = self.events_tx.send(Event::InitialOrder {
378 order: order.clone(),
379 });
380 }
381 } else {
382 for (id, order) in new_map.iter() {
383 if let Some(prev) = self.saved_orders.get(id) {
384 if prev.status != order.status {
385 let _ = self.events_tx.send(Event::OrderStatusChanged {
386 order: order.clone(),
387 });
388 }
389 } else {
390 let _ = self.events_tx.send(Event::NewOrder {
391 order: order.clone(),
392 });
393 if order.status == OrderStatus::Closed {
394 let _ = self.events_tx.send(Event::OrderStatusChanged {
395 order: order.clone(),
396 });
397 }
398 }
399 }
400 }
401 self.saved_orders = new_map;
402 }
403 Err(e) => {
404 log::error!(target: "funpay_client", "Failed to fetch sales list: {}", e);
405 }
406 }
407 first = false;
408 tokio::time::sleep(Duration::from_secs(1)).await;
409 }
410 }
411
412 async fn fetch_sales_list(&self) -> Result<Vec<OrderShortcut>, FunPayError> {
413 let url = "https://funpay.com/orders/trade";
414 let resp = self
415 .client
416 .get(url)
417 .header(
418 reqwest::header::COOKIE,
419 format!("golden_key={}; cookie_prefs=1", self.golden_key),
420 )
421 .header(reqwest::header::USER_AGENT, &self.user_agent)
422 .header(reqwest::header::ACCEPT, "*/*")
423 .send()
424 .await?;
425 if resp.status() == reqwest::StatusCode::FORBIDDEN {
426 return Err(FunPayError::Unauthorized);
427 }
428 if !resp.status().is_success() {
429 let status = resp.status();
430 let body = resp.text().await.unwrap_or_default();
431 return Err(FunPayError::RequestFailed {
432 status,
433 body,
434 url: url.to_string(),
435 });
436 }
437 let body = resp.text().await?;
438 let doc = Html::parse_document(&body);
439 let sel_user = Selector::parse("div.user-link-name").unwrap();
440 if doc.select(&sel_user).next().is_none() {
441 return Err(FunPayError::Unauthorized);
442 }
443 let sel_item = Selector::parse("a.tc-item").unwrap();
444 let sel_order = Selector::parse("div.tc-order").unwrap();
445 let mut out = Vec::new();
446 for a in doc.select(&sel_item) {
447 let class_list: Vec<String> = a.value().classes().map(|s| s.to_string()).collect();
448 let status = if class_list.iter().any(|c| c == "warning") {
449 OrderStatus::Refunded
450 } else if class_list.iter().any(|c| c == "info") {
451 OrderStatus::Paid
452 } else {
453 OrderStatus::Closed
454 };
455 let order_div = a.select(&sel_order).next();
456 if order_div.is_none() {
457 continue;
458 }
459 let mut id_text = order_div.unwrap().text().collect::<String>();
460 id_text = id_text.trim().to_string();
461 let id = if let Some(stripped) = id_text.strip_prefix('#') {
462 stripped.to_string()
463 } else {
464 id_text
465 };
466 out.push(OrderShortcut { id, status });
467 }
468 Ok(out)
469 }
470
471 async fn fetch_chats_histories(
472 &self,
473 chats_data: &[(i64, Option<String>)],
474 ) -> Result<HashMap<i64, Vec<Message>>, FunPayError> {
475 let mut objects = Vec::with_capacity(chats_data.len());
476 for (chat_id, _name) in chats_data.iter() {
477 objects.push(serde_json::json!({
478 "type": "chat_node",
479 "id": chat_id,
480 "tag": "00000000",
481 "data": {"node": chat_id, "last_message": -1, "content": ""}
482 }));
483 }
484 let objects_json = serde_json::to_string(&objects).unwrap();
485 let res = self.post_runner(objects_json).await?;
486 let mut out: HashMap<i64, Vec<Message>> = HashMap::new();
487 let objects = res
488 .get("objects")
489 .and_then(|x| x.as_array())
490 .cloned()
491 .unwrap_or_default();
492 for obj in objects {
493 if obj.get("type").and_then(|x| x.as_str()) != Some("chat_node") {
494 continue;
495 }
496 let id = obj.get("id").and_then(|x| x.as_i64()).unwrap_or(0);
497 let data = obj.get("data");
498 if data.is_none() {
499 out.insert(id, Vec::new());
500 continue;
501 }
502 let data = data.unwrap();
503 let messages = data
504 .get("messages")
505 .and_then(|x| x.as_array())
506 .cloned()
507 .unwrap_or_default();
508 let mut list = Vec::new();
509 for m in messages {
510 let mid = m.get("id").and_then(|x| x.as_i64()).unwrap_or(0);
511 let author_id = m.get("author").and_then(|x| x.as_i64()).unwrap_or(0);
512 let html = m.get("html").and_then(|x| x.as_str()).unwrap_or("");
513 let (text, _image) = Self::parse_message_html(html);
514 list.push(Message {
515 id: mid,
516 chat_id: format!("{}", id),
517 chat_name: chats_data
518 .iter()
519 .find(|(cid, _)| *cid == id)
520 .and_then(|(_, n)| n.clone()),
521 text,
522 interlocutor_id: None,
523 author_id,
524 });
525 }
526 out.insert(id, list);
527 }
528 Ok(out)
529 }
530
531 fn parse_message_html(html: &str) -> (Option<String>, Option<String>) {
532 let html_owned = html.replace("<br>", "\n");
533 let doc = Html::parse_fragment(&html_owned);
534 let sel_text = Selector::parse("div.chat-msg-text").unwrap();
535 if let Some(n) = doc.select(&sel_text).next() {
536 let t = n.text().collect::<String>();
537 return (Some(t), None);
538 }
539 let sel_alert = Selector::parse("div[role=alert]").unwrap();
540 if let Some(n) = doc.select(&sel_alert).next() {
541 let t = n.text().collect::<String>();
542 return (Some(t), None);
543 }
544 let sel_img = Selector::parse("a.chat-img-link").unwrap();
545 if let Some(n) = doc.select(&sel_img).next() {
546 let href = n.value().attr("href").map(|s| s.to_string());
547 return (None, href);
548 }
549 (None, None)
550 }
551}