1use crate::client::http::ReqwestGateway;
2use crate::client::poller::FunPayPoller;
3use crate::client::FunpayGateway;
4use crate::config::FunPayConfig;
5use crate::error::FunPayError;
6use crate::events::Event;
7use crate::models::enums::SubcategoryType;
8use crate::models::ids::ChatId;
9use crate::models::{
10 CategoryFilter, CategorySubcategory, MarketOffer, Message, Offer, OfferEditParams,
11 OfferFullParams, Order, OrderShortcut, Subcategory,
12};
13use crate::parsing::{
14 parse_category_filters, parse_category_subcategories, parse_market_offers, parse_message_html,
15 parse_my_offers, parse_offer_edit_params, parse_offer_full_params, parse_order_page,
16 parse_order_secrets, parse_orders_list,
17};
18use crate::storage::json::JsonFileStorage;
19use crate::storage::memory::InMemoryStorage;
20use crate::storage::StateStorage;
21use crate::utils::{extract_phpsessid, random_tag};
22use regex::Regex;
23use scraper::{Html, Selector};
24use serde_json::{json, to_string, Value};
25use std::collections::HashMap;
26use std::fmt;
27use std::sync::Arc;
28use std::time::Duration;
29use tokio::sync::broadcast::{self, Sender};
30
31#[derive(Debug, Clone)]
32struct AppData {
33 user_id: i64,
34 csrf_token: String,
35}
36
37pub struct FunPayAccount {
38 gateway: Arc<dyn FunpayGateway>,
39 pub golden_key: String,
40 user_agent: String,
41 pub id: Option<i64>,
42 pub username: Option<String>,
43 pub csrf_token: Option<String>,
44 phpsessid: Option<String>,
45 locale: Option<String>,
46 pub events_tx: Sender<Event>,
47 sorted_subcategories: HashMap<SubcategoryType, HashMap<i64, Subcategory>>,
48 storage: Arc<dyn StateStorage>,
49 polling_interval: Duration,
50 error_retry_delay: Duration,
51}
52
53impl fmt::Debug for FunPayAccount {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 f.debug_struct("FunPayAccount")
56 .field("golden_key", &"[redacted]")
57 .field("user_agent", &self.user_agent)
58 .field("id", &self.id)
59 .field("username", &self.username)
60 .finish()
61 }
62}
63
64#[derive(Clone)]
65pub struct FunPaySender {
66 gateway: Arc<dyn FunpayGateway>,
67 golden_key: String,
68 user_agent: String,
69 csrf_token: String,
70 phpsessid: Option<String>,
71 seller_id: i64,
72}
73
74impl FunPaySender {
75 pub fn seller_id(&self) -> i64 {
76 self.seller_id
77 }
78}
79
80impl FunPayAccount {
81 pub fn new(golden_key: String) -> Self {
82 Self::with_config(golden_key, FunPayConfig::default())
83 }
84
85 pub fn with_config(golden_key: String, config: FunPayConfig) -> Self {
86 let gateway: Arc<dyn FunpayGateway> = Arc::new(ReqwestGateway::with_config(&config));
87 Self::with_gateway_and_config(gateway, golden_key, config)
88 }
89
90 pub fn with_proxy(golden_key: String, proxy_url: &str) -> Self {
91 Self::with_proxy_and_config(golden_key, proxy_url, FunPayConfig::default())
92 }
93
94 pub fn with_proxy_and_config(golden_key: String, proxy_url: &str, config: FunPayConfig) -> Self {
95 let gateway: Arc<dyn FunpayGateway> =
96 Arc::new(ReqwestGateway::with_proxy_and_config(proxy_url, &config));
97 Self::with_gateway_and_config(gateway, golden_key, config)
98 }
99
100 pub fn with_gateway(gateway: Arc<dyn FunpayGateway>, golden_key: String) -> Self {
101 Self::with_gateway_and_config(gateway, golden_key, FunPayConfig::default())
102 }
103
104 pub fn with_gateway_and_config(
105 gateway: Arc<dyn FunpayGateway>,
106 golden_key: String,
107 config: FunPayConfig,
108 ) -> Self {
109 let (tx, _rx) = broadcast::channel(config.event_channel_capacity);
110 let storage: Arc<dyn StateStorage> = match config.state_storage_path {
111 Some(ref path) => Arc::new(JsonFileStorage::new(path.clone())),
112 None => Arc::new(InMemoryStorage::new()),
113 };
114 Self {
115 gateway,
116 golden_key,
117 user_agent: config.user_agent.clone(),
118 id: None,
119 username: None,
120 csrf_token: None,
121 phpsessid: None,
122 locale: None,
123 events_tx: tx,
124 sorted_subcategories: HashMap::new(),
125 storage,
126 polling_interval: config.polling_interval,
127 error_retry_delay: config.error_retry_delay,
128 }
129 }
130
131 pub fn subscribe(&self) -> broadcast::Receiver<Event> {
132 self.events_tx.subscribe()
133 }
134
135 pub async fn init(&mut self) -> Result<(), FunPayError> {
136 self.get().await
137 }
138
139 pub fn create_sender(&self) -> Result<FunPaySender, FunPayError> {
140 let csrf = self
141 .csrf_token
142 .as_ref()
143 .ok_or(FunPayError::AccountNotInitiated)?
144 .to_string();
145 let seller_id = self.id.ok_or(FunPayError::AccountNotInitiated)?;
146 Ok(FunPaySender {
147 gateway: self.gateway.clone(),
148 golden_key: self.golden_key.clone(),
149 user_agent: self.user_agent.clone(),
150 csrf_token: csrf,
151 phpsessid: self.phpsessid.clone(),
152 seller_id,
153 })
154 }
155
156 async fn get(&mut self) -> Result<(), FunPayError> {
157 let (body, set_cookies) = self
158 .gateway
159 .get_home(&self.golden_key, &self.user_agent)
160 .await?;
161 if let Some(sess) = extract_phpsessid(&set_cookies) {
162 self.phpsessid = Some(sess);
163 }
164 let html = Html::parse_document(&body);
165 let sel_body = Selector::parse("body").unwrap();
166 let mut app_data: Option<AppData> = None;
167 if let Some(b) = html.select(&sel_body).next() {
168 if let Some(attr) = b.value().attr("data-app-data") {
169 let v: Value =
170 serde_json::from_str(attr).map_err(|e| FunPayError::Parse(e.to_string()))?;
171 let user_id = v
172 .get("userId")
173 .and_then(|x| x.as_i64())
174 .ok_or_else(|| FunPayError::Parse(String::from("missing userId")))?;
175 let csrf = v
176 .get("csrf-token")
177 .and_then(|x| x.as_str())
178 .ok_or_else(|| FunPayError::Parse(String::from("missing csrf-token")))?;
179 if let Some(loc) = v.get("locale").and_then(|x| x.as_str()) {
180 self.locale = Some(loc.to_string());
181 }
182 app_data = Some(AppData {
183 user_id,
184 csrf_token: csrf.to_string(),
185 });
186 }
187 }
188 let sel_uname = Selector::parse("div.user-link-name").unwrap();
189 let username = html
190 .select(&sel_uname)
191 .next()
192 .map(|n| n.text().collect::<String>());
193 if username.is_none() {
194 return Err(FunPayError::Unauthorized);
195 }
196 let app = app_data.ok_or_else(|| FunPayError::Parse(String::from("missing app data")))?;
197 self.id = Some(app.user_id);
198 self.csrf_token = Some(app.csrf_token);
199 self.username = username;
200 self.setup_subcategories(&body);
201 Ok(())
202 }
203
204 fn setup_subcategories(&mut self, html: &str) {
205 let doc = Html::parse_document(html);
206 let sel_lists = Selector::parse("div.promo-game-list").unwrap();
207 let mut lists: Vec<_> = doc.select(&sel_lists).collect();
208 if lists.is_empty() {
209 return;
210 }
211 let container = if lists.len() > 1 {
212 lists.remove(1)
213 } else {
214 lists.remove(0)
215 };
216 let sel_item = Selector::parse("div.promo-game-item").unwrap();
217 let sel_ul = Selector::parse("ul.list-inline").unwrap();
218 let sel_a = Selector::parse("a").unwrap();
219 let re_id = Regex::new(r"/(?:chips|lots)/(\d+)/?").unwrap();
220 for game in container.select(&sel_item) {
221 for ul in game.select(&sel_ul) {
222 for li in ul.children() {
223 if let Some(el) = li.value().as_element() {
224 if el.name() != "li" {
225 continue;
226 }
227 } else {
228 continue;
229 }
230 if let Some(a) = li
231 .first_child()
232 .and_then(|n| n.value().as_element())
233 .and_then(|_| ul.select(&sel_a).next())
234 {
235 let name = a.text().collect::<String>().trim().to_string();
236 if name.is_empty() {
237 continue;
238 }
239 let href = a.value().attr("href").unwrap_or("");
240 let typ = if href.contains("chips/") {
241 SubcategoryType::Currency
242 } else {
243 SubcategoryType::Common
244 };
245 let id = re_id
246 .captures(href)
247 .and_then(|c| c.get(1))
248 .and_then(|m| m.as_str().parse::<i64>().ok());
249 if let Some(sid) = id {
250 let sub = Subcategory {
251 id: Some(sid),
252 name: name.clone(),
253 };
254 let entry = self.sorted_subcategories.entry(typ).or_default();
255 entry.insert(sid, sub);
256 }
257 }
258 }
259 }
260 }
261 }
262
263 pub async fn start_polling_loop(&mut self) -> Result<(), FunPayError> {
264 let poller = FunPayPoller {
265 gateway: self.gateway.clone(),
266 golden_key: self.golden_key.clone(),
267 user_agent: self.user_agent.clone(),
268 id: self.id.ok_or(FunPayError::AccountNotInitiated)?,
269 username: self.username.clone(),
270 csrf_token: self
271 .csrf_token
272 .clone()
273 .ok_or(FunPayError::AccountNotInitiated)?,
274 phpsessid: self.phpsessid.clone(),
275 events_tx: self.events_tx.clone(),
276 storage: self.storage.clone(),
277 polling_interval: self.polling_interval,
278 error_retry_delay: self.error_retry_delay,
279 last_msg_event_tag: random_tag(),
280 last_order_event_tag: random_tag(),
281 last_messages: HashMap::new(),
282 last_messages_ids: HashMap::new(),
283 saved_orders: HashMap::new(),
284 };
285 poller.start().await
286 }
287}
288
289impl FunPaySender {
290 pub async fn send_chat_message(&self, chat_id: &str, content: &str) -> Result<(), FunPayError> {
291 let mut csrf_to_use = self.csrf_token.clone();
292 let mut phpsess_to_use = self.phpsessid.clone();
293 if phpsess_to_use.is_none() {
294 let (body, set_cookies) = self
295 .gateway
296 .get_chat_page(&self.golden_key, &self.user_agent, chat_id)
297 .await?;
298 let html = Html::parse_document(&body);
299 let sel_body = Selector::parse("body").unwrap();
300 if let Some(b) = html.select(&sel_body).next() {
301 if let Some(attr) = b.value().attr("data-app-data") {
302 if let Ok(v) = serde_json::from_str::<Value>(attr) {
303 if let Some(csrf) = v.get("csrf-token").and_then(|x| x.as_str()) {
304 csrf_to_use = csrf.to_string();
305 }
306 }
307 }
308 }
309 if let Some(sess) = extract_phpsessid(&set_cookies) {
310 phpsess_to_use = Some(sess);
311 }
312 }
313 let objects_json = to_string(&vec![serde_json::json!({
314 "type": "chat_node",
315 "id": chat_id,
316 "tag": "00000000",
317 "data": {"node": chat_id, "last_message": -1, "content": ""}
318 })])
319 .unwrap();
320 let request_json = json!({
321 "action": "chat_message",
322 "data": {"node": chat_id, "last_message": -1, "content": content}
323 })
324 .to_string();
325 self.gateway
326 .post_runner(
327 &self.golden_key,
328 &self.user_agent,
329 &csrf_to_use,
330 phpsess_to_use.as_deref(),
331 &objects_json,
332 Some(&request_json),
333 )
334 .await
335 .map(|_| ())
336 }
337
338 pub async fn get_chat_messages(&self, chat_id: &str) -> Result<Vec<Message>, FunPayError> {
339 let objects_json = to_string(&vec![serde_json::json!({
340 "type": "chat_node",
341 "id": chat_id,
342 "tag": "00000000",
343 "data": {"node": chat_id, "last_message": -1, "content": ""}
344 })])
345 .unwrap();
346
347 let res = self
348 .gateway
349 .post_runner(
350 &self.golden_key,
351 &self.user_agent,
352 &self.csrf_token,
353 self.phpsessid.as_deref(),
354 &objects_json,
355 None,
356 )
357 .await?;
358
359 let objects = res
360 .get("objects")
361 .and_then(|x| x.as_array())
362 .cloned()
363 .unwrap_or_default();
364
365 for obj in objects {
366 if obj.get("type").and_then(|x| x.as_str()) != Some("chat_node") {
367 continue;
368 }
369
370 let data = match obj.get("data") {
371 Some(d) => d,
372 None => continue,
373 };
374
375 let messages = data
376 .get("messages")
377 .and_then(|x| x.as_array())
378 .cloned()
379 .unwrap_or_default();
380
381 let mut list = Vec::new();
382 for m in messages {
383 let mid = m.get("id").and_then(|x| x.as_i64()).unwrap_or(0);
384 let author_id = m.get("author").and_then(|x| x.as_i64()).unwrap_or(0);
385 let html = m.get("html").and_then(|x| x.as_str()).unwrap_or("");
386 let (text, _image) = parse_message_html(html);
387 list.push(Message {
388 id: mid,
389 chat_id: ChatId::from(chat_id.to_string()),
390 chat_name: None,
391 text,
392 interlocutor_id: None,
393 author_id,
394 });
395 }
396 return Ok(list);
397 }
398
399 Ok(Vec::new())
400 }
401
402 pub fn get_chat_id_for_user(&self, user_id: i64) -> String {
403 let my_id = self.seller_id;
404 let (id1, id2) = (my_id.min(user_id), my_id.max(user_id));
405 format!("users-{id1}-{id2}")
406 }
407
408 pub async fn get_order_secrets(&self, order_id: &str) -> Result<Vec<String>, FunPayError> {
409 let body = self
410 .gateway
411 .get_order_page(&self.golden_key, &self.user_agent, order_id)
412 .await?;
413 let doc = Html::parse_document(&body);
414 Ok(parse_order_secrets(&doc))
415 }
416
417 pub async fn get_order(&self, order_id: &str) -> Result<Order, FunPayError> {
418 let body = self
419 .gateway
420 .get_order_page(&self.golden_key, &self.user_agent, order_id)
421 .await?;
422 parse_order_page(&body, order_id)
423 }
424
425 pub async fn edit_offer(
426 &self,
427 offer_id: i64,
428 node_id: i64,
429 params: OfferEditParams,
430 ) -> Result<Value, FunPayError> {
431 let html = self
432 .gateway
433 .get_offer_edit_page(&self.golden_key, &self.user_agent, node_id, offer_id)
434 .await?;
435
436 let current = parse_offer_edit_params(&html);
437 log::debug!(
438 target: "funpay_client",
439 "Parsed offer {} current params: quantity={:?}, method={:?}, price={:?}",
440 offer_id,
441 current.quantity,
442 current.method,
443 current.price
444 );
445 let merged = current.merge(params);
446 log::debug!(
447 target: "funpay_client",
448 "Merged offer {} params: quantity={:?}, method={:?}, price={:?}",
449 offer_id,
450 merged.quantity,
451 merged.method,
452 merged.price
453 );
454
455 self.gateway
456 .post_offer_save(
457 &self.golden_key,
458 &self.user_agent,
459 self.phpsessid.as_deref(),
460 &self.csrf_token,
461 offer_id,
462 node_id,
463 &merged,
464 )
465 .await
466 }
467
468 pub async fn get_offer_params(
469 &self,
470 offer_id: i64,
471 node_id: i64,
472 ) -> Result<OfferFullParams, FunPayError> {
473 let html = self
474 .gateway
475 .get_offer_edit_page(&self.golden_key, &self.user_agent, node_id, offer_id)
476 .await?;
477 Ok(parse_offer_full_params(&html, offer_id, node_id))
478 }
479
480 pub async fn get_my_offers(&self, node_id: i64) -> Result<Vec<Offer>, FunPayError> {
481 let html = self
482 .gateway
483 .get_lots_trade_page(&self.golden_key, &self.user_agent, node_id)
484 .await?;
485 Ok(parse_my_offers(&html, node_id))
486 }
487
488 pub async fn get_market_offers(&self, node_id: i64) -> Result<Vec<MarketOffer>, FunPayError> {
489 let html = self
490 .gateway
491 .get_lots_page(&self.golden_key, &self.user_agent, node_id)
492 .await?;
493 Ok(parse_market_offers(&html, node_id))
494 }
495
496 pub async fn get_orders(&self) -> Result<Vec<OrderShortcut>, FunPayError> {
497 let body = self
498 .gateway
499 .get_orders_trade(&self.golden_key, &self.user_agent)
500 .await?;
501 parse_orders_list(&body, self.seller_id)
502 }
503
504 pub async fn get_category_subcategories(
505 &self,
506 node_id: i64,
507 ) -> Result<Vec<CategorySubcategory>, FunPayError> {
508 let html = self
509 .gateway
510 .get_lots_page(&self.golden_key, &self.user_agent, node_id)
511 .await?;
512 Ok(parse_category_subcategories(&html))
513 }
514
515 pub async fn get_category_filters(
516 &self,
517 node_id: i64,
518 ) -> Result<Vec<CategoryFilter>, FunPayError> {
519 let html = self
520 .gateway
521 .get_lots_page(&self.golden_key, &self.user_agent, node_id)
522 .await?;
523 Ok(parse_category_filters(&html))
524 }
525}