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