sticker-convert 2.9.4__py3-none-any.whl → 2.10.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. sticker_convert/cli.py +26 -13
  2. sticker_convert/downloaders/download_base.py +20 -23
  3. sticker_convert/downloaders/download_discord.py +91 -0
  4. sticker_convert/downloaders/download_kakao.py +3 -4
  5. sticker_convert/downloaders/download_line.py +3 -4
  6. sticker_convert/downloaders/download_signal.py +3 -4
  7. sticker_convert/downloaders/download_telegram.py +3 -4
  8. sticker_convert/downloaders/download_viber.py +3 -4
  9. sticker_convert/gui.py +15 -3
  10. sticker_convert/gui_components/frames/cred_frame.py +22 -1
  11. sticker_convert/gui_components/windows/discord_get_auth_window.py +82 -0
  12. sticker_convert/gui_components/windows/signal_get_auth_window.py +39 -85
  13. sticker_convert/job.py +11 -1
  14. sticker_convert/job_option.py +2 -0
  15. sticker_convert/resources/compression.json +94 -0
  16. sticker_convert/resources/help.json +2 -1
  17. sticker_convert/resources/input.json +21 -1
  18. sticker_convert/uploaders/upload_signal.py +5 -4
  19. sticker_convert/uploaders/upload_telegram.py +11 -10
  20. sticker_convert/utils/auth/get_discord_auth.py +115 -0
  21. sticker_convert/utils/auth/get_signal_auth.py +115 -114
  22. sticker_convert/utils/auth/get_viber_auth.py +10 -214
  23. sticker_convert/utils/chrome_remotedebug.py +154 -0
  24. sticker_convert/utils/emoji.py +16 -0
  25. sticker_convert/utils/files/run_bin.py +1 -1
  26. sticker_convert/utils/media/codec_info.py +45 -60
  27. sticker_convert/utils/process.py +187 -0
  28. sticker_convert/utils/url_detect.py +3 -0
  29. sticker_convert/version.py +1 -1
  30. {sticker_convert-2.9.4.dist-info → sticker_convert-2.10.1.dist-info}/METADATA +41 -43
  31. {sticker_convert-2.9.4.dist-info → sticker_convert-2.10.1.dist-info}/RECORD +35 -29
  32. {sticker_convert-2.9.4.dist-info → sticker_convert-2.10.1.dist-info}/LICENSE +0 -0
  33. {sticker_convert-2.9.4.dist-info → sticker_convert-2.10.1.dist-info}/WHEEL +0 -0
  34. {sticker_convert-2.9.4.dist-info → sticker_convert-2.10.1.dist-info}/entry_points.txt +0 -0
  35. {sticker_convert-2.9.4.dist-info → sticker_convert-2.10.1.dist-info}/top_level.txt +0 -0
@@ -3,127 +3,128 @@ import json
3
3
  import os
4
4
  import platform
5
5
  import shutil
6
- from pathlib import Path
7
- from typing import Optional, Tuple
6
+ import time
7
+ import webbrowser
8
+ from typing import Callable, Optional, Tuple, cast
8
9
 
9
- from sqlcipher3 import dbapi2 as sqlite3
10
+ from sticker_convert.definitions import CONFIG_DIR
11
+ from sticker_convert.utils.chrome_remotedebug import CRD
12
+ from sticker_convert.utils.process import killall
10
13
 
11
14
 
12
15
  class GetSignalAuth:
13
- def get_signal_desktop(self) -> Tuple[Optional[str], Optional[str]]:
14
- if platform.system() == "Windows":
15
- signal_bin_path_prod = os.path.expandvars(
16
- "%localappdata%/Programs/signal-desktop/Signal.exe"
17
- )
18
- signal_bin_path_beta = os.path.expandvars(
19
- "%localappdata%/Programs/signal-desktop-beta/Signal Beta.exe"
20
- )
21
- signal_user_data_dir_prod = os.path.abspath(
22
- os.path.expandvars("%appdata%/Signal")
16
+ def __init__(
17
+ self,
18
+ cb_msg: Callable[..., None] = print,
19
+ cb_ask_str: Callable[..., str] = input,
20
+ ):
21
+ chromedriver_download_dir = CONFIG_DIR / "bin"
22
+ os.makedirs(chromedriver_download_dir, exist_ok=True)
23
+
24
+ self.chromedriver_download_dir = chromedriver_download_dir
25
+
26
+ self.cb_ask_str = cb_ask_str
27
+ self.cb_msg = cb_msg
28
+
29
+ def download_signal_desktop(self):
30
+ download_url = "https://signal.org/en/download/"
31
+
32
+ webbrowser.open(download_url)
33
+
34
+ self.cb_msg(download_url)
35
+
36
+ prompt = "Signal Desktop not detected.\n"
37
+ prompt += "Download and install Signal Desktop version\n"
38
+ prompt += "After installation, quit Signal Desktop before continuing"
39
+ if self.cb_ask_str != input:
40
+ self.cb_ask_str(
41
+ prompt, initialvalue=download_url, cli_show_initialvalue=False
23
42
  )
24
- signal_user_data_dir_beta = os.path.abspath(
25
- os.path.expandvars("%appdata%/Signal Beta")
43
+ else:
44
+ self.cb_msg(prompt)
45
+
46
+ def get_signal_bin_path(self) -> Optional[str]:
47
+ signal_paths: Tuple[Optional[str], ...]
48
+ if platform.system() == "Windows":
49
+ signal_paths = (
50
+ os.path.expandvars("%localappdata%/Programs/signal-desktop/Signal.exe"),
51
+ os.path.expandvars(
52
+ "%localappdata%/Programs/signal-desktop-beta/Signal Beta.exe"
53
+ ),
26
54
  )
27
55
  elif platform.system() == "Darwin":
28
- signal_bin_path_prod = "/Applications/Signal.app/Contents/MacOS/Signal"
29
- signal_bin_path_beta = (
30
- "/Applications/Signal Beta.app/Contents/MacOS/Signal Beta"
56
+ signal_paths = (
57
+ "/Applications/Signal.app/Contents/MacOS/Signal",
58
+ "/Applications/Signal Beta.app/Contents/MacOS/Signal Beta",
31
59
  )
32
- signal_user_data_dir_prod = os.path.expanduser(
33
- "~/Library/Application Support/Signal"
34
- )
35
- signal_user_data_dir_beta = os.path.expanduser(
36
- "~/Library/Application Support/Signal Beta"
60
+ else:
61
+ signal_paths = (
62
+ shutil.which("signal-desktop"),
63
+ shutil.which("signal-desktop-beta"),
37
64
  )
65
+
66
+ for signal_path in signal_paths:
67
+ if signal_path is not None and os.path.isfile(signal_path):
68
+ return signal_path
69
+ return None
70
+
71
+ def get_cred(self) -> Tuple[Optional[str], Optional[str]]:
72
+ signal_bin_path = self.get_signal_bin_path()
73
+ if signal_bin_path is None:
74
+ self.download_signal_desktop()
75
+ return None, None
76
+
77
+ if platform.system() == "Windows":
78
+ killall("signal")
38
79
  else:
39
- prod_which = shutil.which("signal-desktop")
40
- if prod_which is None:
41
- signal_bin_path_prod = "signal-desktop"
42
- else:
43
- signal_bin_path_prod = prod_which
44
- beta_which = shutil.which("signal-desktop-beta")
45
- if beta_which is None:
46
- signal_bin_path_beta = "signal-desktop-beta"
47
- else:
48
- signal_bin_path_beta = beta_which
49
- signal_user_data_dir_prod = os.path.expanduser("~/.config/Signal")
50
- signal_user_data_dir_beta = os.path.expanduser("~/.config/Signal Beta")
51
-
52
- if Path(signal_bin_path_prod).is_file():
53
- return signal_bin_path_prod, signal_user_data_dir_prod
54
- if Path(signal_bin_path_beta).is_file():
55
- return signal_bin_path_beta, signal_user_data_dir_beta
56
-
57
- return None, None
58
-
59
- def get_cred(
60
- self,
61
- signal_bin_path: Optional[str] = None,
62
- signal_user_data_dir: Optional[str] = None,
63
- ) -> Tuple[Optional[str], Optional[str], str]:
64
- if not (signal_bin_path and signal_user_data_dir):
65
- signal_bin_path, signal_user_data_dir = self.get_signal_desktop()
66
-
67
- if not (signal_bin_path and signal_user_data_dir):
68
- msg = "Signal Desktop not detected.\n"
69
- msg += "Download and install Signal Desktop,\n"
70
- msg += "then login to Signal Desktop and try again."
71
-
72
- return None, None, msg
73
-
74
- signal_config = Path(signal_user_data_dir, "config.json")
75
-
76
- if not signal_config.is_file():
77
- msg = "Signal Desktop installed,\n"
78
- msg += "but it's config file not found.\n"
79
- msg += "Please login to Signal Desktop and try again.\n"
80
- msg += "\n"
81
- msg += f"{signal_bin_path=}\n"
82
- msg += f"{signal_user_data_dir=}\n"
83
- return None, None, msg
84
-
85
- with open(signal_config, encoding="utf-8") as f:
86
- config = json.load(f)
87
- key = config.get("key")
88
- db_key = f"x'{key}'"
89
-
90
- signal_database = Path(signal_user_data_dir, "sql/db.sqlite")
91
-
92
- if not signal_database.is_file():
93
- msg = "Signal Desktop installed,\n"
94
- msg += "but database file not found.\n"
95
- msg += "Please login to Signal Desktop and try again.\n"
96
- msg += "\n"
97
- msg += f"{signal_bin_path=}\n"
98
- msg += f"{signal_user_data_dir=}\n"
99
- return None, None, msg
100
-
101
- db_conn = sqlite3.connect(signal_database.as_posix())
102
- db_cursor = db_conn.cursor()
103
- db_cursor.execute(f'PRAGMA key="{db_key}"')
104
-
105
- uuid_id = None
106
- result = db_cursor.execute("SELECT * FROM items WHERE id='uuid_id'").fetchone()
107
- if result:
108
- uuid_id = json.loads(result[1])["value"]
109
-
110
- password = None
111
- result = db_cursor.execute("SELECT * FROM items WHERE id='password'").fetchone()
112
- if result:
113
- password = json.loads(result[1])["value"]
114
-
115
- db_conn.close()
116
-
117
- if uuid_id and password:
118
- msg = "Got uuid and password successfully:\n"
119
- msg += f"{uuid_id=}\n"
120
- msg += f"{password=}"
121
- return uuid_id, password, msg
122
-
123
- msg = "Signal Desktop installed and Database found,\n"
124
- msg += "but uuid and password not found.\n"
125
- msg += "Please login to Signal Desktop and try again.\n"
126
- msg += "\n"
127
- msg += f"{signal_bin_path=}\n"
128
- msg += f"{signal_user_data_dir=}\n"
129
- return None, None, msg
80
+ killall("signal-desktop")
81
+
82
+ crd = CRD(signal_bin_path)
83
+ crd.connect()
84
+ # crd.runtime_enable()
85
+ # crd.reload()
86
+ # while True:
87
+ # r = crd.ws.recv()
88
+ # data = json.loads(r)
89
+ # if data.get("method") == "Runtime.executionContextCreated":
90
+ # print(data)
91
+ # if (data.get("method") == "Runtime.executionContextCreated" and
92
+ # data["params"]["context"]["name"] == "Electron Isolated Context"
93
+ # ):
94
+ # context_id = data["params"]["context"]["id"]
95
+ # break
96
+ # crd.runtime_disable()
97
+ context_id = 2
98
+
99
+ uuid, password = None, None
100
+ while True:
101
+ try:
102
+ r = crd.exec_js(
103
+ "window.reduxStore.getState().items.uuid_id", context_id
104
+ )
105
+ except RuntimeError:
106
+ break
107
+ if (
108
+ json.loads(r).get("result", {}).get("result", {}).get("type", "")
109
+ == "string"
110
+ ):
111
+ uuid = cast(str, json.loads(r)["result"]["result"]["value"])
112
+ break
113
+ time.sleep(1)
114
+ while True:
115
+ try:
116
+ r = crd.exec_js(
117
+ "window.reduxStore.getState().items.password", context_id
118
+ )
119
+ except RuntimeError:
120
+ break
121
+ if (
122
+ json.loads(r).get("result", {}).get("result", {}).get("type", "")
123
+ == "string"
124
+ ):
125
+ password = cast(str, json.loads(r)["result"]["result"]["value"])
126
+ break
127
+ time.sleep(1)
128
+
129
+ crd.close()
130
+ return uuid, password
@@ -3,14 +3,14 @@ import importlib.util
3
3
  import os
4
4
  import platform
5
5
  import shutil
6
- import signal
7
6
  import subprocess
8
7
  import time
8
+ from functools import partial
9
9
  from getpass import getpass
10
10
  from pathlib import Path
11
11
  from typing import Callable, List, Optional, Tuple, cast
12
12
 
13
- from sticker_convert.definitions import ROOT_DIR
13
+ from sticker_convert.utils.process import check_admin, find_pid_by_name, get_mem, killall
14
14
 
15
15
  MSG_NO_BIN = """Viber Desktop not detected.
16
16
  Download and install Viber Desktop,
@@ -23,102 +23,10 @@ MSG_SIP_ENABLED = """You need to disable SIP:
23
23
  2. Launch Terminal from the Utilities menu
24
24
  3. Run the command `csrutil disable`
25
25
  4. Restart your computer"""
26
- MSG_NO_PGREP = "pgrep command or psutil python package is necessary"
27
26
  MSG_LAUNCH_FAIL = "Failed to launch Viber"
28
27
  MSG_PERMISSION_ERROR = "Failed to read Viber process memory"
29
28
 
30
29
 
31
- def check_admin_windows() -> bool:
32
- username = os.getenv("username")
33
- if username is None:
34
- return False
35
-
36
- s = subprocess.run(
37
- ["net", "user", username],
38
- capture_output=True,
39
- text=True,
40
- ).stdout
41
-
42
- return True if "*Administrators" in s else False
43
-
44
-
45
- def check_admin_linux() -> bool:
46
- s = subprocess.run(
47
- ["sudo", "-l"],
48
- capture_output=True,
49
- text=True,
50
- ).stdout
51
-
52
- return True if "may run the following commands" in s else False
53
-
54
-
55
- def killall(name: str) -> bool:
56
- result = False
57
-
58
- while True:
59
- pid = find_pid_by_name(name)
60
- if pid is not None:
61
- os.kill(pid, signal.SIGTERM)
62
- result = True
63
- else:
64
- break
65
-
66
- return result
67
-
68
-
69
- def find_pid_by_name(name: str) -> Optional[int]:
70
- if platform.system() == "Windows":
71
- s = subprocess.run(
72
- ["powershell", "-c", "Get-Process", f"*{name}*"],
73
- capture_output=True,
74
- text=True,
75
- ).stdout
76
-
77
- for line in s.split("\n"):
78
- if name in line.lower():
79
- info = name.split()
80
- pid = info[5]
81
- if pid.isnumeric():
82
- return int(pid)
83
-
84
- return None
85
- else:
86
- if platform.system() == "Darwin":
87
- pattern = "Viber"
88
- else:
89
- pattern = ".*[Vv]iber.*"
90
- pid = (
91
- subprocess.run(["pgrep", pattern], capture_output=True, text=True)
92
- .stdout.split("\n")[0]
93
- .strip()
94
- )
95
- if pid == "" or pid.isnumeric() is False:
96
- return None
97
- else:
98
- return int(pid)
99
-
100
-
101
- if importlib.util.find_spec("psutil"):
102
- import psutil
103
-
104
- def killall(name: str) -> bool:
105
- result = False
106
-
107
- for proc in psutil.process_iter(): # type: ignore
108
- if name in proc.name().lower():
109
- proc.kill()
110
- result = True
111
-
112
- return result
113
-
114
- def find_pid_by_name(name: str) -> Optional[int]:
115
- for proc in psutil.process_iter(): # type: ignore
116
- if name in proc.name().lower():
117
- return proc.pid
118
-
119
- return None
120
-
121
-
122
30
  class GetViberAuth:
123
31
  def __init__(self, cb_ask_str: Callable[..., str] = input):
124
32
  self.cb_ask_str = cb_ask_str
@@ -137,113 +45,6 @@ class GetViberAuth:
137
45
 
138
46
  return find_pid_by_name("viber")
139
47
 
140
- def get_mem_windows(self, viber_pid: int) -> Tuple[Optional[bytes], str]:
141
- from pathlib import WindowsPath
142
-
143
- memdump_ps_path = str(WindowsPath(ROOT_DIR / "resources/memdump_windows.ps1"))
144
- arglist = (
145
- f'-NoProfile -ExecutionPolicy Bypass -File "{memdump_ps_path}" {viber_pid}'
146
- )
147
- dump_fpath = os.path.expandvars(f"%temp%/memdump.bin.{viber_pid}")
148
-
149
- cmd = [
150
- "powershell",
151
- "-NoProfile",
152
- "-ExecutionPolicy",
153
- "Bypass",
154
- "-Command",
155
- f"Start-Process -Verb RunAs powershell -ArgumentList '{arglist}'",
156
- ]
157
-
158
- subprocess.run(cmd, capture_output=True, text=True)
159
-
160
- while True:
161
- try:
162
- with open(dump_fpath, "rb") as f:
163
- s = f.read()
164
- if len(s) != 0:
165
- break
166
- time.sleep(1)
167
- except (FileNotFoundError, PermissionError):
168
- pass
169
-
170
- while True:
171
- try:
172
- os.remove(dump_fpath)
173
- break
174
- except PermissionError:
175
- pass
176
-
177
- return s, ""
178
-
179
- def get_mem_linux(self, viber_pid: int) -> Tuple[Optional[bytes], str]:
180
- memdump_sh_path = (ROOT_DIR / "resources/memdump_linux.sh").as_posix()
181
-
182
- s = subprocess.run(
183
- [
184
- memdump_sh_path,
185
- str(viber_pid),
186
- ],
187
- capture_output=True,
188
- ).stdout
189
-
190
- if len(s) > 1000:
191
- pass
192
- elif shutil.which("pkexec") and os.getenv("DISPLAY"):
193
- s = subprocess.run(
194
- [
195
- "pkexec",
196
- memdump_sh_path,
197
- str(viber_pid),
198
- ],
199
- capture_output=True,
200
- ).stdout
201
- else:
202
- prompt = "Enter sudo password: "
203
- if self.cb_ask_str != input:
204
- sudo_password = self.cb_ask_str(
205
- prompt, initialvalue="", cli_show_initialvalue=False
206
- )
207
- else:
208
- sudo_password = getpass(prompt)
209
- sudo_password_pipe = subprocess.Popen(
210
- ("echo", sudo_password), stdout=subprocess.PIPE
211
- )
212
- s = subprocess.run(
213
- [
214
- "sudo",
215
- "-S",
216
- memdump_sh_path,
217
- str(viber_pid),
218
- ],
219
- capture_output=True,
220
- stdin=sudo_password_pipe.stdout,
221
- ).stdout
222
-
223
- return s, ""
224
-
225
- def get_mem_darwin(self, viber_pid: int) -> Tuple[Optional[bytes], str]:
226
- subprocess.run(
227
- [
228
- "lldb",
229
- "--attach-pid",
230
- str(viber_pid),
231
- "-o",
232
- "process save-core /tmp/viber.dmp",
233
- "-o",
234
- "quit",
235
- ],
236
- stdout=subprocess.DEVNULL,
237
- stderr=subprocess.DEVNULL,
238
- )
239
-
240
- with open("/tmp/viber.dmp", "rb") as f:
241
- s = f.read()
242
-
243
- os.remove("/tmp/viber.dmp")
244
-
245
- return s, ""
246
-
247
48
  def get_auth_by_pme(
248
49
  self, viber_bin_path: str, relaunch: bool = True
249
50
  ) -> Tuple[Optional[str], str]:
@@ -315,12 +116,6 @@ class GetViberAuth:
315
116
 
316
117
  if "enabled" in csrutil_status:
317
118
  return None, MSG_SIP_ENABLED
318
- elif (
319
- platform.system() != "Windows"
320
- and shutil.which("pgrep") is None
321
- and importlib.find_loader("psutil") is None
322
- ):
323
- return None, MSG_NO_PGREP
324
119
 
325
120
  if relaunch:
326
121
  viber_pid = self.relaunch_viber(viber_bin_path)
@@ -329,12 +124,13 @@ class GetViberAuth:
329
124
  if viber_pid is None:
330
125
  return None, MSG_LAUNCH_FAIL
331
126
 
332
- if platform.system() == "Windows":
333
- s, msg = self.get_mem_windows(viber_pid)
334
- elif platform.system() == "Darwin":
335
- s, msg = self.get_mem_darwin(viber_pid)
127
+ if self.cb_ask_str == input:
128
+ pw_func = getpass
336
129
  else:
337
- s, msg = self.get_mem_linux(viber_pid)
130
+ pw_func = partial(
131
+ self.cb_ask_str, initialvalue="", cli_show_initialvalue=False
132
+ )
133
+ s, msg = get_mem(viber_pid, pw_func)
338
134
 
339
135
  if s is None:
340
136
  return None, msg
@@ -421,14 +217,14 @@ class GetViberAuth:
421
217
  methods.append(self.get_auth_by_dump)
422
218
  if pme_present:
423
219
  methods.append(self.get_auth_by_pme)
424
- if check_admin_windows() is False:
220
+ if check_admin() is False:
425
221
  methods.reverse()
426
222
  else:
427
223
  if not os.path.isfile("/.dockerenv"):
428
224
  methods.append(self.get_auth_by_dump)
429
225
  if pme_present:
430
226
  methods.append(self.get_auth_by_pme)
431
- if check_admin_linux() is False:
227
+ if check_admin() is False:
432
228
  methods.reverse()
433
229
 
434
230
  for method in methods:
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import os
4
+ import platform
5
+ import shutil
6
+ import socket
7
+ import subprocess
8
+ import time
9
+ from typing import Any, Dict, Optional, Union, cast
10
+
11
+ import requests
12
+ import websocket
13
+
14
+ # References
15
+ # https://github.com/yeongbin-jo/python-chromedriver-autoinstaller/blob/master/chromedriver_autoinstaller/utils.py
16
+ # https://chromedevtools.github.io/devtools-protocol/
17
+
18
+
19
+ def get_free_port() -> int:
20
+ with socket.socket() as sock:
21
+ sock.bind(("", 0))
22
+ port = sock.getsockname()[1]
23
+ return port
24
+
25
+
26
+ class CRD:
27
+ def __init__(self, chrome_bin: str, port: Optional[int] = None):
28
+ if port is None:
29
+ port = get_free_port()
30
+ self.port = port
31
+ self.chrome_proc = subprocess.Popen(
32
+ [
33
+ chrome_bin,
34
+ "--no-sandbox",
35
+ f"--remote-debugging-port={port}",
36
+ f"--remote-allow-origins=http://localhost:{port}",
37
+ ]
38
+ )
39
+
40
+ @staticmethod
41
+ def get_chrome_path() -> Optional[str]:
42
+ chrome_bin: Optional[str]
43
+ if platform.system() == "Darwin":
44
+ chrome_bin = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
45
+
46
+ if os.path.isfile(chrome_bin) is False:
47
+ return None
48
+
49
+ return chrome_bin
50
+
51
+ elif platform.system() == "Windows":
52
+ chrome_x64 = f"{os.environ.get('PROGRAMW6432') or os.environ.get('PROGRAMFILES')}\\Google\\Chrome\\Application"
53
+ chrome_x86 = (
54
+ f"{os.environ.get('PROGRAMFILES(X86)')}\\Google\\Chrome\\Application"
55
+ )
56
+
57
+ chrome_dir = (
58
+ chrome_x64
59
+ if os.path.isdir(chrome_x64)
60
+ else chrome_x86
61
+ if os.path.isdir(chrome_x86)
62
+ else None
63
+ )
64
+
65
+ if chrome_dir is None:
66
+ return None
67
+
68
+ return chrome_dir + "\\chrome.exe"
69
+
70
+ else:
71
+ for executable in (
72
+ "google-chrome",
73
+ "google-chrome-stable",
74
+ "google-chrome-beta",
75
+ "google-chrome-dev",
76
+ "chromium-browser",
77
+ "chromium",
78
+ ):
79
+ chrome_bin = shutil.which(executable)
80
+ if chrome_bin is not None:
81
+ return chrome_bin
82
+ return None
83
+
84
+ def connect(self):
85
+ self.cmd_id = 1
86
+ r = None
87
+ for _ in range(3):
88
+ try:
89
+ r = requests.get(f"http://localhost:{self.port}/json")
90
+ break
91
+ except requests.exceptions.ConnectionError:
92
+ time.sleep(1)
93
+
94
+ if r is None:
95
+ raise RuntimeError("Cannot connect to chrome debugging port")
96
+
97
+ targets = json.loads(r.text)
98
+ self.ws = websocket.create_connection(targets[0]["webSocketDebuggerUrl"]) # type: ignore
99
+
100
+ def send_cmd(self, command: Dict[Any, Any]) -> Union[str, bytes]:
101
+ if command.get("id") is None:
102
+ command["id"] = self.cmd_id
103
+ for _ in range(3):
104
+ try:
105
+ self.ws.send(json.dumps(command))
106
+ r = self.ws.recv()
107
+ self.cmd_id += 1
108
+ return r
109
+ except BrokenPipeError:
110
+ self.connect()
111
+
112
+ raise RuntimeError("Websocket keep disconnecting")
113
+
114
+ def exec_js(self, js: str, context_id: Optional[int] = None):
115
+ command: Dict[str, Any] = {
116
+ "id": self.cmd_id,
117
+ "method": "Runtime.evaluate",
118
+ "params": {"expression": js},
119
+ }
120
+ if context_id is not None:
121
+ command["params"]["contextId"] = context_id
122
+ return self.send_cmd(command)
123
+
124
+ def get_curr_url(self) -> str:
125
+ r = self.exec_js("window.location.href")
126
+ return cast(
127
+ str, json.loads(r).get("result", {}).get("result", {}).get("value", "")
128
+ )
129
+
130
+ def navigate(self, url: str):
131
+ command = {"id": self.cmd_id, "method": "Page.navigate", "params": {"url": url}}
132
+ self.send_cmd(command)
133
+
134
+ def runtime_enable(self):
135
+ command = {
136
+ "method": "Runtime.enable",
137
+ }
138
+ self.send_cmd(command)
139
+
140
+ def runtime_disable(self):
141
+ command = {
142
+ "method": "Runtime.disable",
143
+ }
144
+ self.send_cmd(command)
145
+
146
+ def reload(self):
147
+ command = {
148
+ "method": "Page.reload",
149
+ }
150
+ self.send_cmd(command)
151
+
152
+ def close(self):
153
+ self.ws.close()
154
+ self.chrome_proc.kill()