PySPlus 0.5__tar.gz → 0.6__tar.gz

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.
@@ -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
@@ -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
@@ -6,6 +6,8 @@ PySPlus.egg-info/dependency_links.txt
6
6
  PySPlus.egg-info/requires.txt
7
7
  PySPlus.egg-info/top_level.txt
8
8
  pysplus/Client.py
9
+ pysplus/Update.py
9
10
  pysplus/__init__.py
10
11
  pysplus/async_sync.py
11
- pysplus/colors.py
12
+ pysplus/colors.py
13
+ pysplus/props.py
@@ -1,3 +1,4 @@
1
1
  selenium==4.29.0
2
2
  webdriver_manager==4.0.2
3
3
  bs4==0.0.2
4
+ pytz
@@ -0,0 +1,839 @@
1
+ from selenium import webdriver
2
+ from selenium.webdriver.common.by import By
3
+ from selenium.webdriver.support.ui import WebDriverWait
4
+ from selenium.webdriver.support import expected_conditions as EC
5
+ from selenium.webdriver.chrome.service import Service
6
+ from selenium.webdriver.chrome.options import Options
7
+ from webdriver_manager.chrome import ChromeDriverManager
8
+ from selenium.webdriver.common.action_chains import ActionChains
9
+ import time
10
+ from bs4 import BeautifulSoup
11
+ from .colors import *
12
+ from .async_sync import *
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
27
+
28
+ logging.getLogger('selenium').setLevel(logging.WARNING)
29
+ logging.getLogger('urllib3').setLevel(logging.WARNING)
30
+ logging.getLogger('WDM').setLevel(logging.WARNING)
31
+
32
+ os.environ['WDM_LOG_LEVEL'] = '0'
33
+ os.environ['WDM_PRINT_FIRST_LINE'] = 'False'
34
+
35
+ class Client:
36
+ def __init__(self,
37
+ name_session: str,
38
+ display_welcome=True,
39
+ user_agent: Optional[str] = None,
40
+ time_out: Optional[int] = 60,
41
+ number_phone: Optional[str] = None,
42
+ viewing_browser: Optional[bool] = False
43
+ ):
44
+ self.number_phone = number_phone
45
+ name = name_session + ".pysplus"
46
+ self.name_cookies = name_session + "_cookies.pkl"
47
+ self.viewing_browser = viewing_browser
48
+ self.splus_url = "https://web.splus.ir"
49
+ if os.path.isfile(name):
50
+ with open(name, "r", encoding="utf-8") as file:
51
+ text_json_py_slpus_session = json.load(file)
52
+ self.number_phone = text_json_py_slpus_session["number_phone"]
53
+ self.time_out = text_json_py_slpus_session["time_out"]
54
+ self.user_agent = text_json_py_slpus_session["user_agent"]
55
+ self.display_welcome = text_json_py_slpus_session["display_welcome"]
56
+ else:
57
+ if not number_phone:
58
+ number_phone = input("Enter your phone number : ")
59
+ if number_phone.startswith("0"):
60
+ number_phone = number_phone[1:]
61
+ while number_phone in ["", " ", None] or self.check_phone_number(number_phone)==False:
62
+ cprint("Enter the phone valid !",Colors.RED)
63
+ number_phone = input("Enter your phone number : ")
64
+ if number_phone.startswith("0"):
65
+ number_phone = number_phone[1:]
66
+ is_login = self.login()
67
+ if not is_login:
68
+ print("Error Login !")
69
+ exit()
70
+ # text_json_py_slpus_session = {
71
+ # "name_session": name_session,
72
+ # "number_phone":number_phone,
73
+ # "user_agent": user_agent,
74
+ # "time_out": time_out,
75
+ # "display_welcome": display_welcome,
76
+ # }
77
+ # with open(name, "w", encoding="utf-8") as file:
78
+ # json.dump(
79
+ # text_json_py_slpus_session, file, ensure_ascii=False, indent=4
80
+ # )
81
+ self.time_out = time_out
82
+ self.user_agent = user_agent
83
+ self.number_phone = number_phone
84
+ if display_welcome:
85
+ k = ""
86
+ for text in "Welcome to PySPlus":
87
+ k += text
88
+ print(f"{Colors.GREEN}{k}{Colors.RESET}", end="\r")
89
+ time.sleep(0.07)
90
+ cprint("",Colors.WHITE)
91
+
92
+ def check_phone_number(self,number:str) -> bool:
93
+ if len(number)!=10:
94
+ return False
95
+ if not number.startswith("9"):
96
+ return False
97
+ return True
98
+
99
+ @async_to_sync
100
+ async def login(self) -> bool:
101
+ """لاگین / login"""
102
+ chrome_options = Options()
103
+ if not self.viewing_browser:
104
+ chrome_options.add_argument("--headless")
105
+ chrome_options.add_argument("--start-maximized")
106
+ chrome_options.add_argument("--disable-notifications")
107
+ chrome_options.add_argument("--lang=fa")
108
+ chrome_options.add_experimental_option("detach", True)
109
+ service = Service(ChromeDriverManager().install())
110
+ self.driver = webdriver.Chrome(service=service, options=chrome_options)
111
+ wait = WebDriverWait(self.driver, 30)
112
+ try:
113
+ self.driver.get(self.splus_url)
114
+ wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
115
+ time.sleep(1)
116
+ is_open_cookies = False
117
+ if os.path.exists(self.name_cookies):
118
+ with open(self.name_cookies, 'rb') as file:
119
+ cookies = pickle.load(file)
120
+ for cookie in cookies:
121
+ self.driver.add_cookie(cookie)
122
+ is_open_cookies = True
123
+ if is_open_cookies:
124
+ self.driver.refresh()
125
+ try:
126
+ understand_button = WebDriverWait(self.driver, 3).until(
127
+ EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), 'متوجه شدم')]"))
128
+ )
129
+ understand_button.click()
130
+ time.sleep(1)
131
+ except:
132
+ pass
133
+ phone_input = wait.until(
134
+ EC.presence_of_element_located((By.CSS_SELECTOR, "input#sign-in-phone-number"))
135
+ )
136
+ phone_input.clear()
137
+ phone_number = f"98 98{self.number_phone}"
138
+ phone_input.send_keys(phone_number)
139
+ next_button = wait.until(
140
+ EC.element_to_be_clickable((By.XPATH, "//button[contains(@class, 'Button') and contains(text(), 'بعدی')]"))
141
+ )
142
+ next_button.click()
143
+ time.sleep(5)
144
+ verification_code = input("Enter the Code » ")
145
+ code_input = wait.until(
146
+ EC.presence_of_element_located((By.CSS_SELECTOR, "input#sign-in-code"))
147
+ )
148
+ self.code_html = self.driver.page_source
149
+ code_input.clear()
150
+ code_input.send_keys(verification_code)
151
+ time.sleep(5)
152
+ self.code_html = self.driver.page_source
153
+ messages = await self.get_chat_ids()
154
+ while not messages:
155
+ time.sleep(1)
156
+ self.code_html = self.driver.page_source
157
+ messages = await self.get_chat_ids()
158
+ with open(self.name_cookies, 'wb') as file:
159
+ pickle.dump(self.driver.get_cookies(), file)
160
+ return True
161
+ except Exception as e:
162
+ self.driver.save_screenshot("error_screenshot.png")
163
+ print("ERROR :")
164
+ print(e)
165
+ print("ERROR SAVED : error_screenshot.png")
166
+ return False
167
+
168
+ @async_to_sync
169
+ async def get_url_opened(self) -> str:
170
+ return self.driver.current_url
171
+
172
+ @async_to_sync
173
+ async def get_type_chat_id(
174
+ self,
175
+ chat_id:str
176
+ ) -> Literal["Channel","Group","Bot","User",None]:
177
+ """getting chat id type / گرفتن نوع چت آیدی"""
178
+ if chat_id.startswith("-"):
179
+ if len(chat_id) == 11:
180
+ return "Channel"
181
+ elif len(chat_id) == 12:
182
+ return "Group"
183
+ if len(chat_id) == 6:
184
+ return "User"
185
+ elif len(chat_id) == 8:
186
+ return "Bot"
187
+ return None
188
+
189
+ @async_to_sync
190
+ async def get_chat_ids(self) -> props:
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
196
+ soup = BeautifulSoup(self.code_html, "html.parser")
197
+ root = soup.select_one(
198
+ "body > #UiLoader > div.Transition.full-height > "
199
+ "#Main.left-column-shown.left-column-open > "
200
+ "#LeftColumn > #LeftColumn-main > div.Transition > "
201
+ "div.ChatFolders.not-open.not-shown > div.Transition > "
202
+ "div.chat-list.custom-scroll > div[style*='position: relative']"
203
+ )
204
+ chats = []
205
+ if root:
206
+ divs = root.find_all("div", recursive=True)
207
+ for div in divs:
208
+ anchors = div.find_all("a", href=True)
209
+ for a in anchors:
210
+ if a!=None:
211
+ chat = str(a["href"]).replace("#","")
212
+ chats.append(chat)
213
+ return props(chats)
214
+
215
+ @async_to_sync
216
+ async def get_chats(self) -> props:
217
+ """گرفتن چت ها / getting 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
378
+
379
+ @async_to_sync
380
+ async def open_chat(self, chat_id: str) -> bool:
381
+ """opening chat / باز کردن چت"""
382
+ try:
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)
389
+ WebDriverWait(self.driver, 60).until(
390
+ EC.presence_of_element_located((By.CSS_SELECTOR, "div.chat-list, div[role='main']"))
391
+ )
392
+ chat_link = WebDriverWait(self.driver, 20).until(
393
+ EC.element_to_be_clickable((By.CSS_SELECTOR, f'a[href="#{chat_id}"]'))
394
+ )
395
+ chat_link.click()
396
+ print(f"✅ Chat {chat_id} opened.")
397
+ WebDriverWait(self.driver, 30).until(
398
+ EC.presence_of_element_located((By.CSS_SELECTOR, "div[contenteditable='true']"))
399
+ )
400
+ return True
401
+ except Exception as e:
402
+ print("❌ Error in open_chat : ", e)
403
+ self.driver.save_screenshot("open_chat_error.png")
404
+ return False
405
+
406
+ @async_to_sync
407
+ async def send_text(self, chat_id: str, text: str,reply_message_id: Optional[str]) -> bool:
408
+ """ارسال متن / sending text"""
409
+ try:
410
+ await self.open_chat(chat_id)
411
+ if reply_message_id:
412
+ await self.context_click_message(reply_message_id, menu_text="پاسخ")
413
+ WebDriverWait(self.driver, 25).until(
414
+ EC.presence_of_element_located((By.CSS_SELECTOR, "div[contenteditable='true']"))
415
+ )
416
+ input_box = self.driver.find_element(By.CSS_SELECTOR, "div[contenteditable='true']")
417
+ self.driver.execute_script("""
418
+ arguments[0].innerText = arguments[1];
419
+ arguments[0].dispatchEvent(new Event('input', { bubbles: true }));
420
+ """, input_box, text)
421
+ send_button = WebDriverWait(self.driver, 30).until(
422
+ EC.element_to_be_clickable((
423
+ By.CSS_SELECTOR,
424
+ "button.Button.send.main-button.default.secondary.round.click-allowed"
425
+ ))
426
+ )
427
+ send_button.click()
428
+ print("✅ Message sent successfully.")
429
+ return True
430
+ except Exception as e:
431
+ print(f"❌ Error in send_text : {e}")
432
+ self.driver.save_screenshot("send_text_error.png")
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
+
@@ -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)
@@ -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)
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="PySPlus",
5
- version="0.5",
5
+ version="0.6",
6
6
  author="seyyed mohamad hosein moosavi raja(01)",
7
7
  author_email="mohamadhosein159159@gmail.com",
8
8
  description="the library SPlus platform for bots.",
@@ -11,6 +11,6 @@ setup(
11
11
  url="https://github.com/OandONE/SPlus",
12
12
  packages=find_packages(),
13
13
  python_requires='>=3.8',
14
- install_requires=["selenium==4.29.0","webdriver_manager==4.0.2","bs4==0.0.2"],
14
+ install_requires=["selenium==4.29.0","webdriver_manager==4.0.2","bs4==0.0.2","pytz"],
15
15
  license="MIT"
16
16
  )
@@ -1,267 +0,0 @@
1
- from selenium import webdriver
2
- from selenium.webdriver.common.by import By
3
- from selenium.webdriver.support.ui import WebDriverWait
4
- from selenium.webdriver.support import expected_conditions as EC
5
- from selenium.webdriver.chrome.service import Service
6
- from selenium.webdriver.chrome.options import Options
7
- from webdriver_manager.chrome import ChromeDriverManager
8
- from bs4 import BeautifulSoup
9
- from .colors import *
10
- from .async_sync import *
11
- from typing import Optional,Literal
12
- from traceback import format_exc
13
- import time,os,json,asyncio,logging,pickle
14
-
15
- logging.getLogger('selenium').setLevel(logging.WARNING)
16
- logging.getLogger('urllib3').setLevel(logging.WARNING)
17
- logging.getLogger('WDM').setLevel(logging.WARNING)
18
-
19
- os.environ['WDM_LOG_LEVEL'] = '0'
20
- os.environ['WDM_PRINT_FIRST_LINE'] = 'False'
21
-
22
- class Client:
23
- def __init__(self,
24
- name_session: str,
25
- display_welcome=True,
26
- user_agent: Optional[str] = None,
27
- time_out: Optional[int] = 60,
28
- number_phone: Optional[str] = None
29
- ):
30
- self.number_phone = number_phone
31
- name = name_session + ".pysplus"
32
- self.name_cookies = name_session + "_cookies.pkl"
33
- if os.path.isfile(name):
34
- with open(name, "r", encoding="utf-8") as file:
35
- text_json_py_slpus_session = json.load(file)
36
- self.number_phone = text_json_py_slpus_session["number_phone"]
37
- self.time_out = text_json_py_slpus_session["time_out"]
38
- self.user_agent = text_json_py_slpus_session["user_agent"]
39
- self.display_welcome = text_json_py_slpus_session["display_welcome"]
40
- else:
41
- if not number_phone:
42
- number_phone = input("Enter your phone number : ")
43
- if number_phone.startswith("0"):
44
- number_phone = number_phone[1:]
45
- while number_phone in ["", " ", None] or self.check_phone_number(number_phone)==False:
46
- cprint("Enter the phone valid !",Colors.RED)
47
- number_phone = input("Enter your phone number : ")
48
- if number_phone.startswith("0"):
49
- number_phone = number_phone[1:]
50
- is_login = self.login()
51
- if not is_login:
52
- print("Error Login !")
53
- exit()
54
- # text_json_py_slpus_session = {
55
- # "name_session": name_session,
56
- # "number_phone":number_phone,
57
- # "user_agent": user_agent,
58
- # "time_out": time_out,
59
- # "display_welcome": display_welcome,
60
- # }
61
- # with open(name, "w", encoding="utf-8") as file:
62
- # json.dump(
63
- # text_json_py_slpus_session, file, ensure_ascii=False, indent=4
64
- # )
65
- self.time_out = time_out
66
- self.user_agent = user_agent
67
- self.number_phone = number_phone
68
- if display_welcome:
69
- k = ""
70
- for text in "Welcome to PySPlus":
71
- k += text
72
- print(f"{Colors.GREEN}{k}{Colors.RESET}", end="\r")
73
- time.sleep(0.07)
74
- cprint("",Colors.WHITE)
75
- def check_phone_number(self,number:str) -> bool:
76
- if len(number)!=10:
77
- return False
78
- if not number.startswith("9"):
79
- return False
80
- return True
81
- @async_to_sync
82
- async def login(self) -> bool:
83
- """لاگین / login"""
84
- chrome_options = Options()
85
- chrome_options.add_argument("--headless")
86
- chrome_options.add_argument("--start-maximized")
87
- chrome_options.add_argument("--disable-notifications")
88
- chrome_options.add_argument("--lang=fa")
89
- chrome_options.add_experimental_option("detach", True)
90
- service = Service(ChromeDriverManager().install())
91
- self.driver = webdriver.Chrome(service=service, options=chrome_options)
92
- wait = WebDriverWait(self.driver, 30)
93
- try:
94
- self.driver.get("https://web.splus.ir")
95
- wait.until(lambda d: d.execute_script("return document.readyState") == "complete")
96
- time.sleep(1)
97
- is_open_cookies = False
98
- if os.path.exists(self.name_cookies):
99
- with open(self.name_cookies, 'rb') as file:
100
- cookies = pickle.load(file)
101
- for cookie in cookies:
102
- self.driver.add_cookie(cookie)
103
- is_open_cookies = True
104
- if is_open_cookies:
105
- self.driver.refresh()
106
- try:
107
- understand_button = WebDriverWait(self.driver, 3).until(
108
- EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), 'متوجه شدم')]"))
109
- )
110
- understand_button.click()
111
- time.sleep(1)
112
- except:
113
- pass
114
- phone_input = wait.until(
115
- EC.presence_of_element_located((By.CSS_SELECTOR, "input#sign-in-phone-number"))
116
- )
117
- phone_input.clear()
118
- phone_number = f"98 98{self.number_phone}"
119
- phone_input.send_keys(phone_number)
120
- next_button = wait.until(
121
- EC.element_to_be_clickable((By.XPATH, "//button[contains(@class, 'Button') and contains(text(), 'بعدی')]"))
122
- )
123
- next_button.click()
124
- time.sleep(5)
125
- verification_code = input("Enter the Code » ")
126
- code_input = wait.until(
127
- EC.presence_of_element_located((By.CSS_SELECTOR, "input#sign-in-code"))
128
- )
129
- self.code_html = self.driver.page_source
130
- code_input.clear()
131
- code_input.send_keys(verification_code)
132
- time.sleep(5)
133
- self.code_html = self.driver.page_source
134
- messages = await self.get_chat_ids()
135
- while not messages:
136
- time.sleep(1)
137
- self.code_html = self.driver.page_source
138
- messages = await self.get_chat_ids()
139
- with open(self.name_cookies, 'wb') as file:
140
- pickle.dump(self.driver.get_cookies(), file)
141
- return True
142
- except Exception as e:
143
- print("/*-+123456789*-+")
144
- print(format_exc())
145
- print("123456789*-+")
146
- self.driver.save_screenshot("error_screenshot.png")
147
- print("ERROR :")
148
- print(e)
149
- print("ERROR SAVED : error_screenshot.png")
150
- return False
151
-
152
- @async_to_sync
153
- async def get_type_chat_id(
154
- self,
155
- chat_id:str
156
- ) -> Literal["Channel","Group","Bot","User",None]:
157
- """getting chat id type / گرفتن نوع چت آیدی"""
158
- if chat_id.startswith("-"):
159
- if len(chat_id) == 11:
160
- return "Channel"
161
- elif len(chat_id) == 12:
162
- return "Group"
163
- if len(chat_id) == 6:
164
- return "User"
165
- elif len(chat_id) == 8:
166
- return "Bot"
167
- return None
168
-
169
- @async_to_sync
170
- async def get_chat_ids(self) -> list:
171
- """گرفتن چت آیدی ها / getting chat ids"""
172
- soup = BeautifulSoup(self.code_html, "html.parser")
173
- root = soup.select_one(
174
- "body > #UiLoader > div.Transition.full-height > "
175
- "#Main.left-column-shown.left-column-open > "
176
- "#LeftColumn > #LeftColumn-main > div.Transition > "
177
- "div.ChatFolders.not-open.not-shown > div.Transition > "
178
- "div.chat-list.custom-scroll > div[style*='position: relative']"
179
- )
180
- chats = []
181
- if root:
182
- divs = root.find_all("div", recursive=True)
183
- for div in divs:
184
- anchors = div.find_all("a", href=True)
185
- for a in anchors:
186
- if a!=None:
187
- chat = str(a["href"]).replace("#","")
188
- chats.append(chat)
189
- return chats
190
-
191
- @async_to_sync
192
- async def get_chats(self) -> list:
193
- """گرفتن چت ها / 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
219
-
220
- @async_to_sync
221
- async def open_chat(self, chat_id: str) -> bool:
222
- """opening chat / باز کردن چت"""
223
- try:
224
- self.driver.get("https://web.splus.ir")
225
- WebDriverWait(self.driver, 60).until(
226
- EC.presence_of_element_located((By.CSS_SELECTOR, "div.chat-list, div[role='main']"))
227
- )
228
- chat_link = WebDriverWait(self.driver, 20).until(
229
- EC.element_to_be_clickable((By.CSS_SELECTOR, f'a[href="#{chat_id}"]'))
230
- )
231
- chat_link.click()
232
- print(f"✅ Chat {chat_id} opened.")
233
- WebDriverWait(self.driver, 30).until(
234
- EC.presence_of_element_located((By.CSS_SELECTOR, "div[contenteditable='true']"))
235
- )
236
- return True
237
- except Exception as e:
238
- print("❌ Error in open_chat : ", e)
239
- self.driver.save_screenshot("open_chat_error.png")
240
- return False
241
-
242
- @async_to_sync
243
- async def send_text(self, chat_id: str, text: str) -> bool:
244
- """ارسال متن / sending text"""
245
- try:
246
- await self.open_chat(chat_id)
247
- WebDriverWait(self.driver, 25).until(
248
- EC.presence_of_element_located((By.CSS_SELECTOR, "div[contenteditable='true']"))
249
- )
250
- input_box = self.driver.find_element(By.CSS_SELECTOR, "div[contenteditable='true']")
251
- self.driver.execute_script("""
252
- arguments[0].innerText = arguments[1];
253
- arguments[0].dispatchEvent(new Event('input', { bubbles: true }));
254
- """, input_box, text)
255
- send_button = WebDriverWait(self.driver, 30).until(
256
- EC.element_to_be_clickable((
257
- By.CSS_SELECTOR,
258
- "button.Button.send.main-button.default.secondary.round.click-allowed"
259
- ))
260
- )
261
- send_button.click()
262
- print("✅ Message sent successfully.")
263
- return True
264
- except Exception as e:
265
- print(f"❌ Error in send_text : {e}")
266
- self.driver.save_screenshot("send_text_error.png")
267
- return False
File without changes
File without changes
File without changes
File without changes
File without changes