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.
- {pysplus-0.5 → pysplus-0.6}/PKG-INFO +2 -1
- {pysplus-0.5 → pysplus-0.6}/PySPlus.egg-info/PKG-INFO +2 -1
- {pysplus-0.5 → pysplus-0.6}/PySPlus.egg-info/SOURCES.txt +3 -1
- {pysplus-0.5 → pysplus-0.6}/PySPlus.egg-info/requires.txt +1 -0
- pysplus-0.6/pysplus/Client.py +839 -0
- pysplus-0.6/pysplus/Update.py +7 -0
- pysplus-0.6/pysplus/props.py +44 -0
- {pysplus-0.5 → pysplus-0.6}/setup.py +2 -2
- pysplus-0.5/pysplus/Client.py +0 -267
- {pysplus-0.5 → pysplus-0.6}/PySPlus.egg-info/dependency_links.txt +0 -0
- {pysplus-0.5 → pysplus-0.6}/PySPlus.egg-info/top_level.txt +0 -0
- {pysplus-0.5 → pysplus-0.6}/README.md +0 -0
- {pysplus-0.5 → pysplus-0.6}/pysplus/__init__.py +0 -0
- {pysplus-0.5 → pysplus-0.6}/pysplus/async_sync.py +0 -0
- {pysplus-0.5 → pysplus-0.6}/pysplus/colors.py +0 -0
- {pysplus-0.5 → pysplus-0.6}/setup.cfg +0 -0
@@ -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
|
@@ -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,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,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
|
+
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
|
)
|
pysplus-0.5/pysplus/Client.py
DELETED
@@ -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
|
File without changes
|
File without changes
|