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