PySPlus 0.5__py3-none-any.whl → 0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pysplus/Client.py CHANGED
@@ -5,12 +5,25 @@ from selenium.webdriver.support import expected_conditions as EC
5
5
  from selenium.webdriver.chrome.service import Service
6
6
  from selenium.webdriver.chrome.options import Options
7
7
  from webdriver_manager.chrome import ChromeDriverManager
8
+ from selenium.webdriver.common.action_chains import ActionChains
9
+ import time
8
10
  from bs4 import BeautifulSoup
9
11
  from .colors import *
10
12
  from .async_sync import *
11
- from typing import Optional,Literal
12
- from traceback import format_exc
13
- import time,os,json,asyncio,logging,pickle
13
+ from typing import (
14
+ Optional,
15
+ Literal
16
+ )
17
+ import time
18
+ import os
19
+ import json
20
+ import logging
21
+ import pickle
22
+ import re
23
+ import base64
24
+ import inspect
25
+ from .props import props
26
+ from .Update import Update
14
27
 
15
28
  logging.getLogger('selenium').setLevel(logging.WARNING)
16
29
  logging.getLogger('urllib3').setLevel(logging.WARNING)
@@ -25,11 +38,14 @@ class Client:
25
38
  display_welcome=True,
26
39
  user_agent: Optional[str] = None,
27
40
  time_out: Optional[int] = 60,
28
- number_phone: Optional[str] = None
41
+ number_phone: Optional[str] = None,
42
+ viewing_browser: Optional[bool] = False
29
43
  ):
30
44
  self.number_phone = number_phone
31
45
  name = name_session + ".pysplus"
32
46
  self.name_cookies = name_session + "_cookies.pkl"
47
+ self.viewing_browser = viewing_browser
48
+ self.splus_url = "https://web.splus.ir"
33
49
  if os.path.isfile(name):
34
50
  with open(name, "r", encoding="utf-8") as file:
35
51
  text_json_py_slpus_session = json.load(file)
@@ -72,17 +88,20 @@ class Client:
72
88
  print(f"{Colors.GREEN}{k}{Colors.RESET}", end="\r")
73
89
  time.sleep(0.07)
74
90
  cprint("",Colors.WHITE)
91
+
75
92
  def check_phone_number(self,number:str) -> bool:
76
93
  if len(number)!=10:
77
94
  return False
78
95
  if not number.startswith("9"):
79
96
  return False
80
97
  return True
98
+
81
99
  @async_to_sync
82
100
  async def login(self) -> bool:
83
101
  """لاگین / login"""
84
102
  chrome_options = Options()
85
- chrome_options.add_argument("--headless")
103
+ if not self.viewing_browser:
104
+ chrome_options.add_argument("--headless")
86
105
  chrome_options.add_argument("--start-maximized")
87
106
  chrome_options.add_argument("--disable-notifications")
88
107
  chrome_options.add_argument("--lang=fa")
@@ -91,7 +110,7 @@ class Client:
91
110
  self.driver = webdriver.Chrome(service=service, options=chrome_options)
92
111
  wait = WebDriverWait(self.driver, 30)
93
112
  try:
94
- self.driver.get("https://web.splus.ir")
113
+ self.driver.get(self.splus_url)
95
114
  wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
96
115
  time.sleep(1)
97
116
  is_open_cookies = False
@@ -140,15 +159,16 @@ class Client:
140
159
  pickle.dump(self.driver.get_cookies(), file)
141
160
  return True
142
161
  except Exception as e:
143
- print("/*-+123456789*-+")
144
- print(format_exc())
145
- print("123456789*-+")
146
162
  self.driver.save_screenshot("error_screenshot.png")
147
163
  print("ERROR :")
148
164
  print(e)
149
165
  print("ERROR SAVED : error_screenshot.png")
150
166
  return False
151
167
 
168
+ @async_to_sync
169
+ async def get_url_opened(self) -> str:
170
+ return self.driver.current_url
171
+
152
172
  @async_to_sync
153
173
  async def get_type_chat_id(
154
174
  self,
@@ -167,8 +187,12 @@ class Client:
167
187
  return None
168
188
 
169
189
  @async_to_sync
170
- async def get_chat_ids(self) -> list:
190
+ async def get_chat_ids(self) -> props:
171
191
  """گرفتن چت آیدی ها / getting chat ids"""
192
+ url_opened = await self.get_url_opened()
193
+ if not url_opened == self.splus_url+"/":
194
+ self.driver.get(self.splus_url)
195
+ self.code_html = self.driver.page_source
172
196
  soup = BeautifulSoup(self.code_html, "html.parser")
173
197
  root = soup.select_one(
174
198
  "body > #UiLoader > div.Transition.full-height > "
@@ -186,42 +210,182 @@ class Client:
186
210
  if a!=None:
187
211
  chat = str(a["href"]).replace("#","")
188
212
  chats.append(chat)
189
- return chats
213
+ return props(chats)
190
214
 
191
215
  @async_to_sync
192
- async def get_chats(self) -> list:
216
+ async def get_chats(self) -> props:
193
217
  """گرفتن چت ها / getting chats"""
194
- soup = BeautifulSoup(self.code_html, "html.parser")
195
- root = soup.select_one(
196
- "body > #UiLoader > div.Transition.full-height > "
197
- "#Main.left-column-shown.left-column-open > "
198
- "#LeftColumn > #LeftColumn-main > div.Transition > "
199
- "div.ChatFolders.not-open.not-shown > div.Transition > "
200
- "div.chat-list.custom-scroll > div[style*='position: relative']"
201
- )
202
- chats_ = []
203
- chats = []
204
- if root:
205
- divs = root.find_all("div", recursive=True)
206
- for div in divs:
207
- anchors = div.find_all("a", href=True)
208
- for a in anchors:
209
- if a!=None:
210
- chat = str(a["href"]).replace("#","")
211
- chats_.append(chat)
212
- for chat in chats_:
213
- type_chat = await self.get_type_chat_id(chat)
214
- chats.append({
215
- "chat_id":chat,
216
- "type_chat":type_chat
217
- })
218
- return chats
218
+ try:
219
+ url_opened = await self.get_url_opened()
220
+ if not url_opened == self.splus_url+"/":
221
+ self.driver.get(self.splus_url)
222
+ except Exception:
223
+ pass
224
+ try:
225
+ WebDriverWait(self.driver, 10).until(
226
+ EC.presence_of_element_located((By.CSS_SELECTOR, "div.chat-list.custom-scroll"))
227
+ )
228
+ except Exception:
229
+ pass
230
+ items = self.driver.find_elements(By.CSS_SELECTOR, "div.ListItem.Chat")
231
+ def js_avatar_src(el):
232
+ js = r"""
233
+ return (function(root){
234
+ // تلاش از <img>
235
+ var img = root.querySelector('img.Avatar__media, img.avatar-media, .Avatar img, .avatar img, picture img');
236
+ var src = '';
237
+ if (img){
238
+ src = img.getAttribute('src') || img.currentSrc || img.getAttribute('data-src') || '';
239
+ if (!src){
240
+ var ss = img.getAttribute('srcset') || '';
241
+ if (ss){
242
+ src = ss.split(',')[0].trim().split(' ')[0].trim();
243
+ }
244
+ }
245
+ }
246
+ // اگر نبود، از background-image روی .Avatar
247
+ if (!src){
248
+ var av = root.querySelector('.Avatar, .avatar, .avatar-badge-wrapper');
249
+ if (av){
250
+ var st = getComputedStyle(av);
251
+ var bg = (st && st.backgroundImage) || '';
252
+ if (bg && bg.startsWith('url(')){
253
+ src = bg.slice(4, -1).replace(/^["']|["']$/g,'');
254
+ }
255
+ }
256
+ }
257
+ return src || '';
258
+ })(arguments[0]);
259
+ """
260
+ try:
261
+ return (self.driver.execute_script(js, el) or "").strip()
262
+ except Exception:
263
+ return ""
264
+ results = []
265
+ default_icon_hint = "/person_icon."
266
+ for el in items:
267
+ try:
268
+ try:
269
+ self.driver.execute_script("arguments[0].scrollIntoView({block:'nearest'});", el)
270
+ except Exception:
271
+ pass
272
+ chat_id = ""
273
+ try:
274
+ a = el.find_element(By.CSS_SELECTOR, "a.ListItem-button")
275
+ href = (a.get_attribute("href") or "")
276
+ m = re.search(r"#(\d+)", href)
277
+ if m: chat_id = m.group(1)
278
+ except Exception:
279
+ pass
280
+ if not chat_id:
281
+ try:
282
+ peer = el.find_element(By.CSS_SELECTOR, "[data-peer-id]")
283
+ chat_id = (peer.get_attribute("data-peer-id") or "").strip()
284
+ except Exception:
285
+ chat_id = ""
286
+ try:
287
+ name = el.find_element(By.CSS_SELECTOR, "h3.fullName").text.strip()
288
+ except Exception:
289
+ name = ""
290
+ try:
291
+ time_txt = el.find_element(By.CSS_SELECTOR, "span.time").text.strip()
292
+ except Exception:
293
+ time_txt = ""
294
+ last_message = ""
295
+ try:
296
+ sub_html = self.driver.execute_script(
297
+ "var x=arguments[0].querySelector('.subtitle, p.last-message'); return x? x.innerHTML: '';",
298
+ el
299
+ ) or ""
300
+ soup = BeautifulSoup(sub_html, "html.parser")
301
+ for sp in soup.select("span.Spoiler__content"):
302
+ sp_text = sp.get_text()
303
+ sp.replace_with(f"||{sp_text}||")
304
+ last_message = soup.get_text(" ", strip=True)
305
+ except Exception:
306
+ try:
307
+ last_message = el.find_element(By.CSS_SELECTOR, ".subtitle, p.last-message").text.strip()
308
+ except Exception:
309
+ last_message = ""
310
+ avatar_src = js_avatar_src(el)
311
+ if avatar_src and default_icon_hint in avatar_src:
312
+ try:
313
+ WebDriverWait(self.driver, 0.7).until(
314
+ lambda d: (("blob:" in js_avatar_src(el)) or (default_icon_hint not in js_avatar_src(el)))
315
+ )
316
+ avatar_src = js_avatar_src(el)
317
+ except Exception:
318
+ if default_icon_hint in (avatar_src or ""):
319
+ avatar_src = None
320
+ if not str(avatar_src).startswith("blob:"):
321
+ avatar_src = None
322
+ type_chat = await self.get_type_chat_id(chat_id)
323
+ results.append({
324
+ "chat_id": chat_id,
325
+ "name": name,
326
+ "last_message": {
327
+ "text":last_message,
328
+ "time":time_txt
329
+ },
330
+ "avatar_src": avatar_src,
331
+ "type_chat":type_chat
332
+ })
333
+ except Exception as e:
334
+ try:
335
+ print("get_chats avatar parse error : ", e)
336
+ except:
337
+ pass
338
+ return props(results)
339
+
340
+ @async_to_sync
341
+ async def download_blob_image(self, blob_url: str, dest_path: str) -> bool:
342
+ """download avatar / دانلود آواتور"""
343
+ try:
344
+ js = """
345
+ var url = arguments[0];
346
+ var cb = arguments[arguments.length - 1];
347
+ try {
348
+ var img = new Image();
349
+ img.crossOrigin = 'anonymous';
350
+ img.onload = function(){
351
+ try {
352
+ var canvas = document.createElement('canvas');
353
+ canvas.width = this.naturalWidth || this.width || 0;
354
+ canvas.height = this.naturalHeight || this.height || 0;
355
+ var ctx = canvas.getContext('2d');
356
+ ctx.drawImage(this, 0, 0);
357
+ var data = canvas.toDataURL('image/png').split(',')[1];
358
+ cb(data);
359
+ } catch(e) { cb(null); }
360
+ };
361
+ img.onerror = function(){ cb(null); };
362
+ img.src = url;
363
+ } catch(e) { cb(null); }
364
+ """
365
+ b64 = self.driver.execute_async_script(js, blob_url)
366
+ if not b64:
367
+ return False
368
+ data = base64.b64decode(b64)
369
+ with open(dest_path, "wb") as f:
370
+ f.write(data)
371
+ return True
372
+ except Exception as e:
373
+ try:
374
+ print("download_blob_image error : ", e)
375
+ except:
376
+ pass
377
+ return False
219
378
 
220
379
  @async_to_sync
221
380
  async def open_chat(self, chat_id: str) -> bool:
222
381
  """opening chat / باز کردن چت"""
223
382
  try:
224
- self.driver.get("https://web.splus.ir")
383
+ current = await self.get_url_opened()
384
+ if current == f"{self.splus_url}/#{chat_id}":
385
+ print(f"✅ Chat {chat_id} opened.")
386
+ return True
387
+ if not current == self.splus_url+"/":
388
+ self.driver.get(self.splus_url)
225
389
  WebDriverWait(self.driver, 60).until(
226
390
  EC.presence_of_element_located((By.CSS_SELECTOR, "div.chat-list, div[role='main']"))
227
391
  )
@@ -240,10 +404,12 @@ class Client:
240
404
  return False
241
405
 
242
406
  @async_to_sync
243
- async def send_text(self, chat_id: str, text: str) -> bool:
407
+ async def send_text(self, chat_id: str, text: str,reply_message_id: Optional[str]) -> bool:
244
408
  """ارسال متن / sending text"""
245
409
  try:
246
410
  await self.open_chat(chat_id)
411
+ if reply_message_id:
412
+ await self.context_click_message(reply_message_id, menu_text="پاسخ")
247
413
  WebDriverWait(self.driver, 25).until(
248
414
  EC.presence_of_element_located((By.CSS_SELECTOR, "div[contenteditable='true']"))
249
415
  )
@@ -264,4 +430,410 @@ class Client:
264
430
  except Exception as e:
265
431
  print(f"❌ Error in send_text : {e}")
266
432
  self.driver.save_screenshot("send_text_error.png")
267
- return False
433
+ return False
434
+
435
+ @async_to_sync
436
+ async def get_chat(
437
+ self,
438
+ chat_id
439
+ ) -> props:
440
+ """getting messages chat / گرفتن پیام های چت"""
441
+ opening = await self.open_chat(chat_id)
442
+ type_chat = await self.get_type_chat_id(chat_id)
443
+ peer_name = None
444
+ peer_status = None
445
+ peer_avatar = None
446
+ peer_verified = False
447
+ if not opening:
448
+ return props(
449
+ {
450
+ "messages":[],
451
+ "chat":{
452
+ "name": peer_name,
453
+ "avatar_src": peer_avatar,
454
+ "last_seen": peer_status,
455
+ "verified": peer_verified,
456
+ "type": type_chat
457
+ }
458
+ }
459
+ )
460
+ try:
461
+ header_el = WebDriverWait(self.driver, 5).until(
462
+ lambda d: d.find_element(By.CSS_SELECTOR, ".ChatInfo")
463
+ )
464
+ header_html = self.driver.execute_script("return arguments[0].outerHTML;", header_el)
465
+ hsoup = BeautifulSoup(header_html, "html.parser")
466
+ name_tag = hsoup.select_one(".fullName, .title h3, .info h3")
467
+ if name_tag:
468
+ peer_name = name_tag.get_text(strip=True)
469
+ status_tag = hsoup.select_one(".user-status, .status, .info .status")
470
+ if status_tag:
471
+ peer_status = status_tag.get_text(" ", strip=True)
472
+ if hsoup.select_one("svg.VerifiedIcon"):
473
+ peer_verified = True
474
+ try:
475
+ avatar_src = self.driver.execute_script("""
476
+ var root = arguments[0];
477
+ var img = root.querySelector('.Avatar__media, .avatar-media, .Avatar img, .avatar img, picture img');
478
+ if (img) {
479
+ var s = img.getAttribute('src') || img.currentSrc || img.getAttribute('data-src') || '';
480
+ if (s) return s;
481
+ }
482
+ var av = root.querySelector('.Avatar, .avatar, .Avatar.size-medium, .Avatar.size-large');
483
+ if (av) {
484
+ var st = getComputedStyle(av);
485
+ var bg = st && st.backgroundImage || '';
486
+ if (bg && bg.indexOf('url(') === 0) {
487
+ return bg.slice(4, -1).replace(/^['"]|['"]$/g,'');
488
+ }
489
+ }
490
+ return '';
491
+ """, header_el) or ""
492
+ peer_avatar = avatar_src or None
493
+ except Exception:
494
+ peer_avatar = None
495
+ except Exception:
496
+ pass
497
+ try:
498
+ WebDriverWait(self.driver, 20).until(
499
+ EC.presence_of_element_located((By.CSS_SELECTOR, ".messages-container"))
500
+ )
501
+ except Exception:
502
+ pass
503
+ try:
504
+ WebDriverWait(self.driver, 15).until(
505
+ EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".Message, .message-list-item"))
506
+ )
507
+ except Exception:
508
+ pass
509
+ try:
510
+ script_scroll = """
511
+ (function(){
512
+ var el = document.querySelector('.messages-container');
513
+ if(!el) return false;
514
+ el.scrollTop = el.scrollHeight;
515
+ return true;
516
+ })();
517
+ """
518
+ for _ in range(3):
519
+ try:
520
+ self.driver.execute_script(script_scroll)
521
+ time.sleep(0.35)
522
+ except Exception:
523
+ break
524
+ except Exception:
525
+ pass
526
+ time.sleep(0.5)
527
+ try:
528
+ container_el = self.driver.find_element(By.CSS_SELECTOR, ".messages-container")
529
+ html_fragment = self.driver.execute_script("return arguments[0].innerHTML;", container_el)
530
+ html_string = f'<div class="messages-container">{html_fragment}</div>'
531
+ except Exception:
532
+ html_string = self.driver.page_source
533
+ def normalize_lines(text):
534
+ lines = [ln.strip() for ln in text.splitlines()]
535
+ lines = [ln for ln in lines if ln]
536
+ return "\n".join(lines)
537
+ def extract_text_from_textcontent(text_div):
538
+ if text_div is None:
539
+ return ""
540
+ for meta in text_div.select(".MessageMeta"):
541
+ meta.extract()
542
+ raw = text_div.get_text("\n", strip=True)
543
+ return normalize_lines(raw)
544
+ def _persian_digits_to_ascii(s: str) -> str:
545
+ if not s:
546
+ return s
547
+ persian_offset = {ord(c): ord('0') + i for i, c in enumerate("۰۱۲۳۴۵۶۷۸۹")}
548
+ arabic_offset = {ord(c): ord('0') + i for i, c in enumerate("٠١٢٣٤٥٦٧٨٩")}
549
+ table = {}
550
+ table.update(persian_offset)
551
+ table.update(arabic_offset)
552
+ return s.translate(table)
553
+ def parse_message_tag(msg_tag, sticky_text=None):
554
+ time_span = msg_tag.select_one(".message-time")
555
+ time_sent = None
556
+ full_date = None
557
+ if time_span:
558
+ title = time_span.get("title") or ""
559
+ title = title.strip()
560
+ if title:
561
+ title_ascii = _persian_digits_to_ascii(title)
562
+ if "،" in title_ascii:
563
+ parts = [p.strip() for p in title_ascii.split("،") if p.strip()]
564
+ else:
565
+ parts = [p.strip() for p in title_ascii.split(",") if p.strip()]
566
+ if len(parts) >= 2:
567
+ date_part = "، ".join(parts[:-1]) if len(parts) > 2 else parts[0]
568
+ time_part = parts[-1]
569
+ full_date = title_ascii
570
+ time_sent = time_part
571
+ else:
572
+ full_date = title_ascii
573
+ import re
574
+ m = re.search(r'(\d{1,2}[:\:\uFF1A]\d{2}(?::\d{2})?)', title_ascii)
575
+ if m:
576
+ time_sent = m.group(1).replace("\uFF1A", ":")
577
+ else:
578
+ txt = time_span.get_text(strip=True)
579
+ txt_ascii = _persian_digits_to_ascii(txt)
580
+ if ":" in txt_ascii:
581
+ parts = txt_ascii.split(":")
582
+ if len(parts) == 2:
583
+ time_sent = f"{parts[0].zfill(2)}:{parts[1].zfill(2)}:00"
584
+ else:
585
+ time_sent = txt_ascii
586
+ else:
587
+ time_sent = txt_ascii
588
+ text_div = msg_tag.select_one(".text-content")
589
+ if text_div is None:
590
+ text_div = msg_tag.select_one(".content-inner")
591
+ cleaned = extract_text_from_textcontent(text_div)
592
+ classes = msg_tag.get("class", []) or []
593
+ own_flag = False
594
+ if "own" in classes:
595
+ own_flag = True
596
+ if msg_tag.select_one(".with-outgoing-icon") or msg_tag.select_one(".MessageOutgoingStatus") or msg_tag.select_one(".MessageOutgoingStatus .icon-message-succeeded"):
597
+ own_flag = True
598
+ summary = cleaned.replace("\n", " ")
599
+ if len(summary) > 160:
600
+ summary = summary[:157].rstrip() + "..."
601
+ return {
602
+ "message_id": msg_tag.get("id").replace("message", "") if msg_tag.get("id") else None,
603
+ "day": sticky_text,
604
+ "date": full_date,
605
+ "time": time_sent,
606
+ "is_me": bool(own_flag),
607
+ "text": cleaned,
608
+ "summary": summary,
609
+ "classes": classes
610
+ }
611
+
612
+ soup = BeautifulSoup(html_string, "html.parser")
613
+ container = soup.select_one(".messages-container") or soup
614
+ sticky_current = None
615
+ collected = []
616
+ seen_ids = set()
617
+ for d in container.find_all("div", recursive=True):
618
+ d_classes = d.get("class") or []
619
+ if "sticky-date" in d_classes:
620
+ txt = d.get_text(" ", strip=True)
621
+ sticky_current = txt if txt else sticky_current
622
+ continue
623
+ is_msg = False
624
+ for token in ("Message", "message-list-item"):
625
+ if token in d_classes:
626
+ is_msg = True
627
+ break
628
+ if is_msg:
629
+ mid = d.get("id")
630
+ if mid and mid in seen_ids:
631
+ continue
632
+ parsed = parse_message_tag(d, sticky_text=sticky_current)
633
+ collected.append(parsed)
634
+ if mid:
635
+ seen_ids.add(mid)
636
+ collected.reverse()
637
+ return props(
638
+ {
639
+ "messages":collected,
640
+ "chat":{
641
+ "name": peer_name,
642
+ "avatar_src": peer_avatar,
643
+ "last_seen": peer_status,
644
+ "verified": peer_verified,
645
+ "type": type_chat
646
+ }
647
+ }
648
+ )
649
+
650
+ def _dispatch_js_contextmenu(self, el):
651
+ js = """
652
+ var el = arguments[0];
653
+ var ev = document.createEvent('MouseEvent');
654
+ ev.initMouseEvent('contextmenu', true, true, window, 1, 0,0,0,0, false, false, false, false, 2, null);
655
+ el.dispatchEvent(ev);
656
+ return true;
657
+ """
658
+ try:
659
+ return self.driver.execute_script(js, el)
660
+ except Exception:
661
+ return False
662
+
663
+ @async_to_sync
664
+ async def context_click_message(
665
+ self,
666
+ message_id: str,
667
+ menu_selector: Optional[str] = None,
668
+ menu_text: Optional[str] = None,
669
+ timeout: int = 8
670
+ ) -> bool:
671
+ try:
672
+ mid = str(message_id)
673
+ if not mid.startswith("message"):
674
+ mid = "message" + mid
675
+ msg_el = WebDriverWait(self.driver, 5).until(
676
+ lambda d: d.find_element(By.ID, mid)
677
+ )
678
+ try:
679
+ self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", msg_el)
680
+ except Exception:
681
+ pass
682
+ time.sleep(0.12)
683
+ try:
684
+ ac = ActionChains(self.driver)
685
+ ac.move_to_element(msg_el).context_click(msg_el).perform()
686
+ except Exception:
687
+ self._dispatch_js_contextmenu(msg_el)
688
+ wait = WebDriverWait(self.driver, timeout)
689
+ if menu_selector:
690
+ item = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, menu_selector)))
691
+ item.click()
692
+ return True
693
+ if menu_text:
694
+ xpaths = [
695
+ f"//button[normalize-space()='{menu_text}']",
696
+ f"//div[normalize-space()='{menu_text}']",
697
+ f"//a[normalize-space()='{menu_text}']",
698
+ f"//*[normalize-space()='{menu_text}']"
699
+ ]
700
+ for xp in xpaths:
701
+ try:
702
+ el = wait.until(EC.element_to_be_clickable((By.XPATH, xp)))
703
+ el.click()
704
+ return True
705
+ except Exception:
706
+ continue
707
+ return False
708
+ return True
709
+ except Exception as e:
710
+ try:
711
+ self.driver.save_screenshot("context_click_error.png")
712
+ except:
713
+ pass
714
+ return False
715
+
716
+ @async_to_sync
717
+ async def click_confirm(
718
+ self,
719
+ confirm_selector: Optional[str] = None,
720
+ confirm_text: Optional[str] = None,
721
+ timeout: int = 6,
722
+ take_screenshot_on_fail: bool = True
723
+ ) -> bool:
724
+ try:
725
+ wait = WebDriverWait(self.driver, timeout)
726
+ if confirm_selector:
727
+ try:
728
+ btn = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, confirm_selector)))
729
+ btn.click()
730
+ return True
731
+ except Exception:
732
+ pass
733
+ candidate_texts = []
734
+ if confirm_text:
735
+ candidate_texts.append(confirm_text)
736
+ candidate_texts += ["حذف", "حذف پیام", "حذف گفتگو", "بله", "تایید", "OK", "Yes", "Delete", "Confirm"]
737
+ xpaths = []
738
+ for t in candidate_texts:
739
+ xpaths.extend([
740
+ f"//button[normalize-space()='{t}']",
741
+ f"//a[normalize-space()='{t}']",
742
+ f"//div[normalize-space()='{t}']",
743
+ f"//span[normalize-space()='{t}']",
744
+ f"//*[@role='button' and normalize-space()='{t}']"
745
+ ])
746
+ for xp in xpaths:
747
+ try:
748
+ el = wait.until(EC.element_to_be_clickable((By.XPATH, xp)))
749
+ el.click()
750
+ return True
751
+ except Exception:
752
+ continue
753
+ try:
754
+ buttons = self.driver.find_elements(By.TAG_NAME, "button")
755
+ low_candidates = [t.lower() for t in candidate_texts]
756
+ for b in buttons:
757
+ try:
758
+ txt = (b.text or "").strip().lower()
759
+ if not txt:
760
+ val = b.get_attribute("value") or ""
761
+ txt = val.strip().lower()
762
+ for cand in low_candidates:
763
+ if cand and cand in txt:
764
+ try:
765
+ b.click()
766
+ return True
767
+ except:
768
+ try:
769
+ self.driver.execute_script("arguments[0].click();", b)
770
+ return True
771
+ except:
772
+ pass
773
+ except Exception:
774
+ continue
775
+ except Exception:
776
+ pass
777
+ if take_screenshot_on_fail:
778
+ try:
779
+ self.driver.save_screenshot("confirm_click_failed.png")
780
+ except:
781
+ pass
782
+ return False
783
+ except Exception:
784
+ try:
785
+ self.driver.save_screenshot("confirm_click_error.png")
786
+ except:
787
+ pass
788
+ return False
789
+
790
+ @async_to_sync
791
+ async def delete_message(self,message_id:str,chat_id:str) -> bool:
792
+ """delete message / حذف پیام"""
793
+ opening = await self.open_chat(chat_id)
794
+ if opening:
795
+ try:
796
+ click_right = await self.context_click_message(message_id, menu_text="حذف")
797
+ if click_right:
798
+ delete = await self.click_confirm(confirm_text="حذف")
799
+ if delete:
800
+ return True
801
+ return False
802
+ except:
803
+ raise ValueError("Invalid Acsses")
804
+ else:
805
+ return False
806
+
807
+ @async_to_sync
808
+ async def pin_message(self,message_id:str,chat_id:str) -> bool:
809
+ """pining message / سنجاق پیام"""
810
+ type_chat = await self.get_type_chat_id(chat_id)
811
+ if type_chat in ["Group","Channel"]:
812
+ await self.open_chat(chat_id)
813
+ try:
814
+ click_right = await self.context_click_message(message_id, menu_text="سنجاق کردن")
815
+ if click_right:
816
+ pining = await self.click_confirm(confirm_text="سنجاق کردن")
817
+ if pining:
818
+ return True
819
+ return False
820
+ except:
821
+ raise ValueError("Invalid Acsses")
822
+ raise ValueError("group and channel can pining message")
823
+
824
+ @async_to_sync
825
+ async def unpin_message(self,message_id:str,chat_id:str) -> bool:
826
+ """unpining message / برداشتن سنجاق پیام"""
827
+ type_chat = await self.get_type_chat_id(chat_id)
828
+ if type_chat in ["Group","Channel"]:
829
+ await self.open_chat(chat_id)
830
+ try:
831
+ click_right = await self.context_click_message(message_id, menu_text="برداشتن سنجاق")
832
+ if click_right:
833
+ return True
834
+ return False
835
+ except:
836
+ raise ValueError("Invalid Acsses")
837
+ raise ValueError("group and channel can pining message")
838
+
839
+
pysplus/Update.py ADDED
@@ -0,0 +1,7 @@
1
+ import json
2
+
3
+ class Update:
4
+ def __init__(self,data):
5
+ self._data_ = data
6
+ def __str__(self) -> str:
7
+ return json.dumps(self._data_,indent=4,ensure_ascii=False)
pysplus/props.py ADDED
@@ -0,0 +1,44 @@
1
+ import json
2
+
3
+ class props:
4
+ def __init__(self,data):
5
+ self._data_ = data
6
+ def __getattr__(self, name):
7
+ return self.find_prop(keys=name)
8
+ def __getitem__(self, key):
9
+ return self._data_[key]
10
+ def __lts__(self, update: list, *args, **kwargs):
11
+ for index, element in enumerate(update):
12
+ if isinstance(element, list):
13
+ update[index] = self.__lts__(update=element)
14
+ elif isinstance(element, dict):
15
+ update[index] = props(data=element)
16
+ else:
17
+ update[index] = element
18
+ return update
19
+ def find_prop(self, keys, data_=None, *args, **kwargs):
20
+ if data_ is None:
21
+ data_ = self._data_
22
+ if not isinstance(keys, list):
23
+ keys = [keys]
24
+ if isinstance(data_, dict):
25
+ for key in keys:
26
+ try:
27
+ update = data_[key]
28
+ if isinstance(update, dict):
29
+ update = props(data=update)
30
+ elif isinstance(update, list):
31
+ update = self.__lts__(update=update)
32
+ return update
33
+ except KeyError:
34
+ pass
35
+ data_ = data_.values()
36
+ for value in data_:
37
+ if isinstance(value, (dict, list)):
38
+ try:
39
+ return self.find_prop(keys=keys, data_=value)
40
+ except AttributeError:
41
+ return None
42
+ return None
43
+ def __str__(self) -> str:
44
+ return json.dumps(self._data_,indent=4,ensure_ascii=False)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySPlus
3
- Version: 0.5
3
+ Version: 0.6
4
4
  Summary: the library SPlus platform for bots.
5
5
  Home-page: https://github.com/OandONE/SPlus
6
6
  Author: seyyed mohamad hosein moosavi raja(01)
@@ -11,6 +11,7 @@ Description-Content-Type: text/markdown
11
11
  Requires-Dist: selenium==4.29.0
12
12
  Requires-Dist: webdriver_manager==4.0.2
13
13
  Requires-Dist: bs4==0.0.2
14
+ Requires-Dist: pytz
14
15
  Dynamic: author
15
16
  Dynamic: author-email
16
17
  Dynamic: description
@@ -0,0 +1,10 @@
1
+ pysplus/Client.py,sha256=NOef2SKn1FQNKEHIk4cx4TUDfh-mRMI81zjai_UGBRk,35602
2
+ pysplus/Update.py,sha256=4mUzfYwICFsPKCwJ3T_cvqDB07Sr9okIF0Ib5Vuu-QA,185
3
+ pysplus/__init__.py,sha256=D6b12F-lYPY38W7ymcNtCgUQb7tu79scbEcbDjIRR5c,97
4
+ pysplus/async_sync.py,sha256=d9VSE7_A7zgOI8BAlZoxfm8LdkdWxyZdpgAEOrRpQcg,489
5
+ pysplus/colors.py,sha256=SwcpovYEff7gDHMtWZAjYnVSbvhqSiHXlAoa4eSJ9RM,556
6
+ pysplus/props.py,sha256=BN171ElaDSjRn1pfIQ2zkVTUS_0MBLrZn-EiNOw12tM,1644
7
+ pysplus-0.6.dist-info/METADATA,sha256=vVRKPDrM8bmGC42uanMlLA5QI0WYA0BN8AluUoPNVHk,875
8
+ pysplus-0.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ pysplus-0.6.dist-info/top_level.txt,sha256=h2PKSbKoDxAHsNE8pxuQY8VllsI-4KQW_Udcd7DKQkA,8
10
+ pysplus-0.6.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- pysplus/Client.py,sha256=QuNKFuDiSaysh93_0wfXKsY_PGCg1sEqYGdvuTLQRaU,11552
2
- pysplus/__init__.py,sha256=D6b12F-lYPY38W7ymcNtCgUQb7tu79scbEcbDjIRR5c,97
3
- pysplus/async_sync.py,sha256=d9VSE7_A7zgOI8BAlZoxfm8LdkdWxyZdpgAEOrRpQcg,489
4
- pysplus/colors.py,sha256=SwcpovYEff7gDHMtWZAjYnVSbvhqSiHXlAoa4eSJ9RM,556
5
- pysplus-0.5.dist-info/METADATA,sha256=yqXKWChLEIVeFXJR5-l4anNFJyA2z8pjHVmT2EVM0Sc,854
6
- pysplus-0.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- pysplus-0.5.dist-info/top_level.txt,sha256=h2PKSbKoDxAHsNE8pxuQY8VllsI-4KQW_Udcd7DKQkA,8
8
- pysplus-0.5.dist-info/RECORD,,
File without changes