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 +612 -40
- pysplus/Update.py +7 -0
- pysplus/props.py +44 -0
- {pysplus-0.5.dist-info → pysplus-0.6.dist-info}/METADATA +2 -1
- pysplus-0.6.dist-info/RECORD +10 -0
- pysplus-0.5.dist-info/RECORD +0 -8
- {pysplus-0.5.dist-info → pysplus-0.6.dist-info}/WHEEL +0 -0
- {pysplus-0.5.dist-info → pysplus-0.6.dist-info}/top_level.txt +0 -0
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
|
12
|
-
|
13
|
-
|
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
|
-
|
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(
|
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) ->
|
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) ->
|
216
|
+
async def get_chats(self) -> props:
|
193
217
|
"""گرفتن چت ها / getting chats"""
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
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.
|
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
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.
|
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,,
|
pysplus-0.5.dist-info/RECORD
DELETED
@@ -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
|
File without changes
|