sticker-convert 2.13.3.0__py3-none-any.whl → 2.15.0.0__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.
Files changed (41) hide show
  1. sticker_convert/auth/__init__.py +0 -0
  2. sticker_convert/auth/auth_base.py +19 -0
  3. sticker_convert/{utils/auth/get_discord_auth.py → auth/auth_discord.py} +40 -13
  4. sticker_convert/{utils/auth/get_kakao_auth.py → auth/auth_kakao_android_login.py} +80 -84
  5. sticker_convert/auth/auth_kakao_desktop_login.py +323 -0
  6. sticker_convert/{utils/auth/get_kakao_desktop_auth.py → auth/auth_kakao_desktop_memdump.py} +21 -12
  7. sticker_convert/{utils/auth/get_line_auth.py → auth/auth_line.py} +21 -6
  8. sticker_convert/{utils/auth/get_signal_auth.py → auth/auth_signal.py} +18 -20
  9. sticker_convert/auth/auth_telethon.py +151 -0
  10. sticker_convert/{utils/auth/get_viber_auth.py → auth/auth_viber.py} +19 -11
  11. sticker_convert/{utils/auth → auth}/telegram_api.py +10 -18
  12. sticker_convert/cli.py +57 -67
  13. sticker_convert/converter.py +4 -4
  14. sticker_convert/downloaders/download_line.py +2 -2
  15. sticker_convert/downloaders/download_telegram.py +1 -1
  16. sticker_convert/gui.py +20 -100
  17. sticker_convert/gui_components/frames/comp_frame.py +12 -4
  18. sticker_convert/gui_components/frames/config_frame.py +14 -6
  19. sticker_convert/gui_components/frames/control_frame.py +1 -1
  20. sticker_convert/gui_components/frames/cred_frame.py +6 -8
  21. sticker_convert/gui_components/windows/advanced_compression_window.py +3 -4
  22. sticker_convert/gui_components/windows/base_window.py +7 -2
  23. sticker_convert/gui_components/windows/discord_get_auth_window.py +3 -7
  24. sticker_convert/gui_components/windows/kakao_get_auth_window.py +272 -97
  25. sticker_convert/gui_components/windows/line_get_auth_window.py +5 -14
  26. sticker_convert/gui_components/windows/signal_get_auth_window.py +4 -12
  27. sticker_convert/gui_components/windows/viber_get_auth_window.py +8 -11
  28. sticker_convert/job.py +16 -32
  29. sticker_convert/job_option.py +1 -0
  30. sticker_convert/resources/NotoColorEmoji.ttf +0 -0
  31. sticker_convert/resources/help.json +8 -6
  32. sticker_convert/uploaders/upload_telegram.py +1 -1
  33. sticker_convert/utils/callback.py +238 -6
  34. sticker_convert/version.py +1 -1
  35. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.15.0.0.dist-info}/METADATA +41 -42
  36. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.15.0.0.dist-info}/RECORD +40 -37
  37. sticker_convert/utils/auth/telethon_setup.py +0 -97
  38. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.15.0.0.dist-info}/WHEEL +0 -0
  39. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.15.0.0.dist-info}/entry_points.txt +0 -0
  40. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.15.0.0.dist-info}/licenses/LICENSE +0 -0
  41. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.15.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env python3
2
+ import base64
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import platform
7
+ import re
8
+ import shutil
9
+ import socket
10
+ import subprocess
11
+ import time
12
+ import uuid
13
+ from typing import Any, Optional, Tuple
14
+
15
+ import requests
16
+ from bs4 import BeautifulSoup
17
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
18
+
19
+ from sticker_convert.auth.auth_base import AuthBase
20
+
21
+ OK_MSG = "Login successful, auth_token: {auth_token}"
22
+
23
+
24
+ class AuthKakaoDesktopLogin(AuthBase):
25
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
26
+ super().__init__(*args, **kwargs)
27
+ self.username = self.opt_cred.kakao_username
28
+ self.password = self.opt_cred.kakao_password
29
+
30
+ if platform.system() == "Darwin":
31
+ self.plat = "mac"
32
+ user_agent_os = "Mc"
33
+ self.os_version = platform.mac_ver()[0]
34
+ version = self.macos_get_ver()
35
+ if version is None:
36
+ version = "25.2.0"
37
+ else:
38
+ self.plat = "win32"
39
+ user_agent_os = "Wd"
40
+ if platform.system() == "Windows":
41
+ self.os_version = platform.uname().version
42
+ else:
43
+ self.os_version = "10.0"
44
+ version = self.windows_get_ver()
45
+ if version is None:
46
+ version = "25.8.2"
47
+
48
+ user_agent = f"KT/{version} {user_agent_os}/{self.os_version} en"
49
+ self.headers = {
50
+ "user-agent": user_agent,
51
+ "a": f"{self.plat}/{version}/en",
52
+ "accept": "*/*",
53
+ "accept-encoding": "gzip, deflate, br",
54
+ "accept-language": "en",
55
+ "host": "katalk.kakao.com",
56
+ "Connection": "close",
57
+ }
58
+
59
+ self.device_name = socket.gethostname()
60
+ if platform.system() != "Darwin":
61
+ self.device_name = self.device_name.upper()
62
+ self.device_uuid = self.get_device_uuid()
63
+
64
+ hash = hashlib.sha512(
65
+ f"KLEAL|{self.username}|{self.device_uuid}|LCNUE|{user_agent}".encode(
66
+ "utf-8"
67
+ )
68
+ ).hexdigest()
69
+ xvc = hash[:16]
70
+
71
+ self.headers_login = self.headers.copy()
72
+ self.headers_login["x-vc"] = xvc
73
+
74
+ def pkcs7_pad(self, m: str) -> str:
75
+ return m + chr(16 - len(m) % 16) * (16 - len(m) % 16)
76
+
77
+ def windows_get_ver(self) -> Optional[str]:
78
+ r = requests.get(
79
+ "https://api.github.com/repos/microsoft/winget-pkgs/contents/manifests/k/Kakao/KakaoTalk"
80
+ )
81
+ rjson = json.loads(r.text)
82
+ if len(rjson) == 0:
83
+ return None
84
+ ver = rjson[-1]["name"]
85
+ return ".".join(ver.split(".")[:3])
86
+
87
+ def macos_get_ver(self) -> Optional[str]:
88
+ country = "us"
89
+ app_name = "kakaotalk-messenger"
90
+ app_id = "362057947"
91
+
92
+ url = f"https://apps.apple.com/{country}/app/{app_name}/id{app_id}"
93
+
94
+ r = requests.get(url)
95
+ soup = BeautifulSoup(r.text, "html.parser")
96
+ version_tag = soup.find("p", {"class": re.compile(".*latest__version")})
97
+ version_str = None
98
+ if version_tag is not None:
99
+ version_str = version_tag.text.replace("Version ", "")
100
+ return version_str
101
+
102
+ def windows_get_pragma(self, use_wine: bool = False) -> Optional[str]:
103
+ sys_uuid = None
104
+ hdd_model = None
105
+ hdd_serial = None
106
+
107
+ if use_wine and shutil.which("wine") is None:
108
+ return None
109
+
110
+ regkey = "HKEY_CURRENT_USER\\Software\\Kakao\\KakaoTalk\\DeviceInfo"
111
+ wine = "wine " if use_wine else ""
112
+ cmd = f"{wine}reg query '{regkey}' /v Last"
113
+ last_device_info = subprocess.run(
114
+ cmd, stdout=subprocess.PIPE, shell=True
115
+ ).stdout.decode()
116
+ if "REG_SZ" not in last_device_info:
117
+ return None
118
+ last_device_info = last_device_info.split("\n")[2].split()[2]
119
+
120
+ if self.opt_cred.kakao_device_uuid:
121
+ sys_uuid = self.opt_cred.kakao_device_uuid
122
+ else:
123
+ cmd = f"{wine}reg query '{regkey}\\{last_device_info}' /v sys_uuid"
124
+ sys_uuid = subprocess.run(
125
+ cmd, stdout=subprocess.PIPE, shell=True
126
+ ).stdout.decode()
127
+ if "REG_SZ" in sys_uuid:
128
+ sys_uuid = sys_uuid.split("\n")[2].split()[2]
129
+
130
+ cmd = f"{wine}reg query '{regkey}\\{last_device_info}' /v hdd_model"
131
+ hdd_model = subprocess.run(
132
+ cmd, stdout=subprocess.PIPE, shell=True
133
+ ).stdout.decode()
134
+ if "REG_SZ" in hdd_model:
135
+ hdd_model = hdd_model.split("\n")[2].split()[2]
136
+
137
+ cmd = f"{wine}reg query '{regkey}\\{last_device_info}' /v hdd_serial"
138
+ hdd_serial = subprocess.run(
139
+ cmd, stdout=subprocess.PIPE, shell=True
140
+ ).stdout.decode()
141
+ if "REG_SZ" in hdd_serial:
142
+ hdd_serial = hdd_serial.split("\n")[2].split()[2]
143
+
144
+ if sys_uuid and hdd_model and hdd_serial:
145
+ return f"{sys_uuid}|{hdd_model}|{hdd_serial}"
146
+ else:
147
+ return None
148
+
149
+ def get_device_uuid(self) -> str:
150
+ if platform.system() == "Darwin":
151
+ cmd = "ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID | awk '{print $3}'"
152
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True, check=True)
153
+ hwid = result.stdout.decode().strip().replace('"', "")
154
+
155
+ hwid_sha1 = bytes.fromhex(hashlib.sha1(hwid.encode()).hexdigest())
156
+ hwid_sha256 = bytes.fromhex(hashlib.sha256(hwid.encode()).hexdigest())
157
+
158
+ return base64.b64encode(hwid_sha1 + hwid_sha256).decode()
159
+ else:
160
+ use_wine = True if platform.system != "Windows" else False
161
+ pragma = self.windows_get_pragma(use_wine=use_wine)
162
+ if pragma is None:
163
+ if platform.system == "Windows":
164
+ if self.opt_cred.kakao_device_uuid:
165
+ sys_uuid = self.opt_cred.kakao_device_uuid
166
+ else:
167
+ cmd = "wmic csproduct get uuid"
168
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
169
+ sys_uuid = result.stdout.decode().split("\n")[1].strip()
170
+ cmd = "wmic diskdrive get Model"
171
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
172
+ hdd_model = result.stdout.decode().split("\n")[1].strip()
173
+ cmd = "wmic diskdrive get SerialNumber"
174
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
175
+ hdd_serial = result.stdout.decode().split("\n")[1].strip()
176
+ else:
177
+ if self.opt_cred.kakao_device_uuid:
178
+ sys_uuid = self.opt_cred.kakao_device_uuid
179
+ else:
180
+ product_uuid_path = "/sys/devices/virtual/dmi/id/product_uuid"
181
+ sys_uuid = None
182
+ if os.access(product_uuid_path, os.R_OK):
183
+ cmd = f"cat {product_uuid_path}"
184
+ result = subprocess.run(
185
+ cmd, stdout=subprocess.PIPE, shell=True
186
+ )
187
+ if result.returncode == 0:
188
+ sys_uuid = result.stdout.decode().strip()
189
+ if sys_uuid is None:
190
+ sys_uuid = str(uuid.uuid4()).upper()
191
+ self.opt_cred.kakao_device_uuid = sys_uuid
192
+ hdd_model = "Wine Disk Drive"
193
+ hdd_serial = ""
194
+ pragma = f"{sys_uuid}|{hdd_model}|{hdd_serial}"
195
+
196
+ aes_key = bytes.fromhex("9FBAE3118FDE5DEAEB8279D08F1D4C79")
197
+ aes_iv = bytes.fromhex("00000000000000000000000000000000")
198
+
199
+ cipher = Cipher(algorithms.AES(aes_key), modes.CBC(aes_iv))
200
+ encryptor = cipher.encryptor()
201
+ padded = self.pkcs7_pad(pragma).encode()
202
+ encrypted_pragma = encryptor.update(padded) + encryptor.finalize()
203
+ pragma_hash = hashlib.sha512(encrypted_pragma).digest()
204
+
205
+ return base64.b64encode(pragma_hash).decode()
206
+
207
+ def login(self, forced: bool = False):
208
+ data = {
209
+ "device_name": self.device_name,
210
+ "device_uuid": self.device_uuid,
211
+ "email": self.username,
212
+ "os_version": self.os_version,
213
+ "password": self.password,
214
+ "permanent": "1",
215
+ }
216
+
217
+ if forced:
218
+ data["forced"] = "1"
219
+
220
+ headers = self.headers_login.copy()
221
+ headers["content-type"] = "application/x-www-form-urlencoded"
222
+ response = requests.post(
223
+ f"https://katalk.kakao.com/{self.plat}/account/login.json",
224
+ headers=headers,
225
+ data=data,
226
+ )
227
+
228
+ return json.loads(response.text)
229
+
230
+ def generate_passcode(self):
231
+ data = {
232
+ "password": self.password,
233
+ "permanent": True,
234
+ "device": {
235
+ "name": self.device_name,
236
+ "osVersion": self.os_version,
237
+ "uuid": self.device_uuid,
238
+ },
239
+ "email": self.username,
240
+ }
241
+
242
+ if platform.system() == "Darwin":
243
+ cmd = "sysctl -n hw.model"
244
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True, check=True)
245
+ data["device"]["model"] = result.stdout.decode().strip() # type: ignore
246
+
247
+ headers = self.headers_login.copy()
248
+ headers["content-type"] = "application/json"
249
+ response = requests.post(
250
+ f"https://katalk.kakao.com/{self.plat}/account/passcodeLogin/generate",
251
+ headers=headers,
252
+ json=data,
253
+ )
254
+
255
+ return json.loads(response.text)
256
+
257
+ def register_device(self):
258
+ data = {
259
+ "device": {"uuid": self.device_uuid},
260
+ "email": self.username,
261
+ "password": self.password,
262
+ }
263
+
264
+ headers = self.headers_login.copy()
265
+ headers["content-type"] = "application/json"
266
+ response = None
267
+
268
+ response = requests.post(
269
+ f"https://katalk.kakao.com/{self.plat}/account/passcodeLogin/registerDevice",
270
+ headers=headers,
271
+ json=data,
272
+ )
273
+ return json.loads(response.text)
274
+
275
+ def get_cred(self) -> Tuple[Optional[str], str]:
276
+ msg = "Getting Kakao authorization token by desktop login..."
277
+ self.cb.put(("msg_dynamic", (msg,), None))
278
+ rjson = self.login()
279
+ access_token = rjson.get("access_token")
280
+ if access_token is not None:
281
+ auth_token = access_token + "-" + self.device_uuid
282
+ return auth_token, OK_MSG.format(auth_token=auth_token)
283
+
284
+ rjson = self.generate_passcode()
285
+ if rjson.get("status") != 0:
286
+ return None, f"Failed to generate passcode: {rjson}"
287
+ passcode = rjson["passcode"]
288
+
289
+ fail_reason = None
290
+ self.cb.put(("msg_dynamic", (None,), None))
291
+ while True:
292
+ rjson = self.register_device()
293
+ if rjson["status"] == 0:
294
+ break
295
+ elif rjson["status"] == -110:
296
+ fail_reason = "Timeout"
297
+ break
298
+ elif rjson["status"] != -100:
299
+ fail_reason = str(rjson)
300
+ break
301
+ time_remaining = rjson.get("remainingSeconds")
302
+ next_req_time = rjson.get("nextRequestIntervalInSeconds")
303
+ if time_remaining is None or next_req_time is None:
304
+ fail_reason = str(rjson)
305
+ msg = f"Please enter passcode in Kakao app on mobile device within {time_remaining} seconds: {passcode}"
306
+ msg_dynamic_window_exist = self.cb.put(("msg_dynamic", (msg,), None))
307
+ if msg_dynamic_window_exist is False:
308
+ fail_reason = "Cancelled"
309
+ break
310
+ time.sleep(next_req_time)
311
+ self.cb.put(("msg_dynamic", (None,), None))
312
+ if fail_reason is not None:
313
+ return None, f"Failed to register device: {fail_reason}"
314
+
315
+ rjson = self.login()
316
+ if rjson.get("status") == -101:
317
+ rjson = self.login(forced=True)
318
+ access_token = rjson.get("access_token")
319
+ if access_token is None:
320
+ return None, f"Failed to login after registering device: {rjson}"
321
+
322
+ auth_token = access_token + "-" + self.device_uuid
323
+ return auth_token, OK_MSG.format(auth_token=auth_token)
@@ -4,11 +4,10 @@ import platform
4
4
  import re
5
5
  import subprocess
6
6
  import time
7
- from functools import partial
8
- from getpass import getpass
9
7
  from pathlib import Path
10
- from typing import Callable, Optional, Tuple, Union, cast
8
+ from typing import Any, Optional, Tuple, Union, cast
11
9
 
10
+ from sticker_convert.auth.auth_base import AuthBase
12
11
  from sticker_convert.utils.process import find_pid_by_name, get_mem, killall
13
12
 
14
13
  MSG_NO_BIN = """Kakao Desktop not detected.
@@ -26,9 +25,9 @@ MSG_LAUNCH_FAIL = "Failed to launch Kakao"
26
25
  MSG_PERMISSION_ERROR = "Failed to read Kakao process memory"
27
26
 
28
27
 
29
- class GetKakaoDesktopAuth:
30
- def __init__(self, cb_ask_str: Callable[..., str] = input) -> None:
31
- self.cb_ask_str = cb_ask_str
28
+ class AuthKakaoDesktopMemdump(AuthBase):
29
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
30
+ super().__init__(*args, **kwargs)
32
31
 
33
32
  def launch_kakao(self, kakao_bin_path: str) -> None:
34
33
  if platform.system() == "Windows":
@@ -111,12 +110,18 @@ class GetKakaoDesktopAuth:
111
110
  if relaunch and self.relaunch_kakao(kakao_bin_path) is None:
112
111
  return None, MSG_LAUNCH_FAIL
113
112
 
114
- if self.cb_ask_str == input:
115
- pw_func = getpass
116
- else:
117
- pw_func = partial(
118
- self.cb_ask_str, initialvalue="", cli_show_initialvalue=False
113
+ def pw_func(msg: str) -> str:
114
+ return self.cb.put(
115
+ (
116
+ "ask_str",
117
+ None,
118
+ {
119
+ "message": msg,
120
+ "password": True,
121
+ },
122
+ )
119
123
  )
124
+
120
125
  s = get_mem(kakao_pid, pw_func, is_wine)
121
126
 
122
127
  if s is None:
@@ -129,7 +134,7 @@ class GetKakaoDesktopAuth:
129
134
  auth_token_bytes = s[auth_token_addr : auth_token_addr + 200]
130
135
  auth_token_term = auth_token_bytes.find(b"\x00")
131
136
  if auth_token_term == -1:
132
- return None, MSG_NO_AUTH
137
+ continue
133
138
  auth_token_candidate = auth_token_bytes[:auth_token_term].decode(
134
139
  encoding="ascii"
135
140
  )
@@ -245,6 +250,8 @@ class GetKakaoDesktopAuth:
245
250
  # - Slow
246
251
  # - Cannot run on macOS
247
252
 
253
+ msg = "Getting Kakao authorization token by Desktop memdump...\n(This may take a minute)"
254
+ self.cb.put(("msg_dynamic", (msg,), None))
248
255
  if not kakao_bin_path:
249
256
  kakao_bin_path = self.get_kakao_desktop()
250
257
 
@@ -260,4 +267,6 @@ class GetKakaoDesktopAuth:
260
267
  else:
261
268
  kakao_auth, msg = self.get_auth_by_dump(kakao_bin_path)
262
269
 
270
+ self.cb.put(("msg_dynamic", (None,), None))
271
+
263
272
  return kakao_auth, msg
@@ -2,14 +2,25 @@
2
2
  import json
3
3
  import platform
4
4
  from http.cookiejar import CookieJar
5
- from typing import Any, Callable, Dict, List, Optional, Union
5
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
6
6
 
7
7
  import requests
8
8
  import rookiepy
9
9
 
10
+ from sticker_convert.auth.auth_base import AuthBase
11
+
12
+ OK_MSG = "Got Line cookies successfully"
13
+ FAIL_MSG = "Failed to get Line cookies. Have you logged in the web browser?"
14
+
15
+
16
+ class AuthLine(AuthBase):
17
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
18
+ super().__init__(*args, **kwargs)
19
+
20
+ def get_cred(self) -> Tuple[Optional[str], str]:
21
+ msg = "Getting Line cookies"
22
+ self.cb.put(("msg_dynamic", (msg,), None))
10
23
 
11
- class GetLineAuth:
12
- def get_cred(self) -> Optional[str]:
13
24
  browsers: List[Callable[..., Any]] = [
14
25
  rookiepy.load, # Supposed to load from any browser, but may fail
15
26
  rookiepy.firefox,
@@ -44,19 +55,23 @@ class GetLineAuth:
44
55
  cookies_dict = browser(["store.line.me"])
45
56
  cookies_jar = rookiepy.to_cookiejar(cookies_dict)
46
57
 
47
- if GetLineAuth.validate_cookies(cookies_jar):
58
+ if AuthLine.validate_cookies(cookies_jar):
48
59
  break
49
60
 
50
61
  except Exception:
51
62
  continue
52
63
 
64
+ self.cb.put(("msg_dynamic", (None,), None))
53
65
  if cookies_dict is None or cookies_jar is None:
54
- return None
66
+ return (
67
+ None,
68
+ "Failed to get Line cookies. Have you logged in the web browser?",
69
+ )
55
70
 
56
71
  cookies_list = ["%s=%s" % (i["name"], i["value"]) for i in cookies_dict]
57
72
  cookies = ";".join(cookies_list)
58
73
 
59
- return cookies
74
+ return cookies, OK_MSG
60
75
 
61
76
  @staticmethod
62
77
  def validate_cookies(cookies: Union[CookieJar, Dict[str, str]]) -> bool:
@@ -5,43 +5,33 @@ import platform
5
5
  import shutil
6
6
  import time
7
7
  import webbrowser
8
- from typing import Callable, Optional, Tuple, cast
8
+ from typing import Any, Optional, Tuple, cast
9
9
 
10
+ from sticker_convert.auth.auth_base import AuthBase
10
11
  from sticker_convert.definitions import CONFIG_DIR
11
12
  from sticker_convert.utils.chrome_remotedebug import CRD
12
13
  from sticker_convert.utils.process import killall
13
14
 
14
15
 
15
- class GetSignalAuth:
16
- def __init__(
17
- self,
18
- cb_msg: Callable[..., None] = print,
19
- cb_ask_str: Callable[..., str] = input,
20
- ) -> None:
16
+ class AuthSignal(AuthBase):
17
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
18
+ super().__init__(*args, **kwargs)
21
19
  chromedriver_download_dir = CONFIG_DIR / "bin"
22
20
  os.makedirs(chromedriver_download_dir, exist_ok=True)
23
21
 
24
22
  self.chromedriver_download_dir = chromedriver_download_dir
25
23
 
26
- self.cb_ask_str = cb_ask_str
27
- self.cb_msg = cb_msg
28
-
29
24
  def download_signal_desktop(self) -> None:
30
25
  download_url = "https://signal.org/en/download/"
31
26
 
32
27
  webbrowser.open(download_url)
33
28
 
34
- self.cb_msg(download_url)
29
+ self.cb.put(download_url)
35
30
 
36
31
  prompt = "Signal Desktop not detected.\n"
37
32
  prompt += "Download and install Signal Desktop version\n"
38
33
  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
42
- )
43
- else:
44
- self.cb_msg(prompt)
34
+ self.cb.put(("ask_str", (prompt,), None))
45
35
 
46
36
  def get_signal_bin_path(self) -> Optional[str]:
47
37
  signal_paths: Tuple[Optional[str], ...]
@@ -73,11 +63,14 @@ class GetSignalAuth:
73
63
  return signal_path
74
64
  return None
75
65
 
76
- def get_cred(self) -> Tuple[Optional[str], Optional[str]]:
66
+ def get_cred(self) -> Tuple[Optional[str], Optional[str], str]:
67
+ msg = "Getting Signal credentials..."
68
+ self.cb.put(("msg_dynamic", (msg,), None))
77
69
  signal_bin_path = self.get_signal_bin_path()
78
70
  if signal_bin_path is None:
71
+ self.cb.put(("msg_dynamic", (None,), None))
79
72
  self.download_signal_desktop()
80
- return None, None
73
+ return None, None, "Failed to get uuid and password"
81
74
 
82
75
  if platform.system() == "Windows":
83
76
  killall("signal")
@@ -132,4 +125,9 @@ class GetSignalAuth:
132
125
  time.sleep(1)
133
126
 
134
127
  crd.close()
135
- return uuid, password
128
+ self.cb.put(("msg_dynamic", (None,), None))
129
+ return (
130
+ uuid,
131
+ password,
132
+ f"Got uuid and password successfully:\nuuid={uuid}\npassword={password}",
133
+ )
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env python3
2
+ from typing import Any, Optional, Tuple
3
+
4
+ import anyio
5
+ from telethon import TelegramClient # type: ignore
6
+ from telethon.errors import RPCError, SessionPasswordNeededError # type: ignore
7
+
8
+ from sticker_convert.auth.auth_base import AuthBase
9
+ from sticker_convert.definitions import CONFIG_DIR
10
+
11
+ GUIDE_MSG = """1. Visit https://my.telegram.org
12
+ 2. Login using your phone number
13
+ 3. Go to "API development tools"
14
+ 4. Fill form
15
+ - App title: sticker-convert
16
+ - Short name: sticker-convert
17
+ - URL: www.telegram.org
18
+ - Platform: Desktop
19
+ - Description: sticker-convert
20
+ 5. Note down api_id and api_hash
21
+ Continue when done"""
22
+ OK_MSG = "Telethon setup successful"
23
+ FAIL_MSG = "Telethon setup failed"
24
+
25
+
26
+ class AuthTelethon(AuthBase):
27
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
28
+ super().__init__(*args, **kwargs)
29
+
30
+ async def signin_async(
31
+ self, try_sign_in: bool = True
32
+ ) -> Tuple[bool, TelegramClient, int, str, str]:
33
+ client = TelegramClient(
34
+ CONFIG_DIR / f"telethon-{self.opt_cred.telethon_api_id}.session",
35
+ self.opt_cred.telethon_api_id,
36
+ self.opt_cred.telethon_api_hash,
37
+ )
38
+
39
+ await client.connect()
40
+ authed = await client.is_user_authorized()
41
+ if authed is False and try_sign_in is True:
42
+ error_msg = ""
43
+ while True:
44
+ msg = f"{error_msg}Enter phone number: "
45
+ phone_number = self.cb.put(("ask_str", (msg,), None))
46
+ if phone_number == "":
47
+ return False, client, 0, "", FAIL_MSG
48
+ try:
49
+ await client.send_code_request(phone_number)
50
+ break
51
+ except RPCError as e:
52
+ error_msg = f"Error: {e}\n"
53
+
54
+ error_msg = ""
55
+ while True:
56
+ msg = f"{error_msg}Enter code: "
57
+ code = self.cb.put(("ask_str", (msg,), None))
58
+ if code == "":
59
+ return False, client, 0, "", FAIL_MSG
60
+ try:
61
+ await client.sign_in(phone_number, code)
62
+ break
63
+ except SessionPasswordNeededError:
64
+ password = self.cb.put(
65
+ (
66
+ "ask_str",
67
+ None,
68
+ {"question": "Enter password: ", "password": True},
69
+ )
70
+ )
71
+ try:
72
+ await client.sign_in(password=password)
73
+ break
74
+ except RPCError as e:
75
+ error_msg = f"Error: {e}\n"
76
+ except RPCError as e:
77
+ error_msg = f"Error: {e}\n"
78
+ authed = await client.is_user_authorized()
79
+
80
+ return (
81
+ authed,
82
+ client,
83
+ self.opt_cred.telethon_api_id,
84
+ self.opt_cred.telethon_api_hash,
85
+ OK_MSG if authed else FAIL_MSG,
86
+ )
87
+
88
+ def guide(self) -> None:
89
+ self.cb.put(("msg_block", (GUIDE_MSG,), None))
90
+
91
+ def get_api_info(self) -> bool:
92
+ api_id_ask = "Enter api_id: "
93
+ wrong_hint = ""
94
+
95
+ while True:
96
+ telethon_api_id = self.cb.put(
97
+ (
98
+ "ask_str",
99
+ None,
100
+ {
101
+ "question": wrong_hint + api_id_ask,
102
+ "initialvalue": str(self.opt_cred.telethon_api_id),
103
+ },
104
+ )
105
+ )
106
+ if telethon_api_id == "":
107
+ return False
108
+ elif telethon_api_id.isnumeric():
109
+ self.opt_cred.telethon_api_id = int(telethon_api_id)
110
+ break
111
+ else:
112
+ wrong_hint = "Error: api_id should be numeric\n"
113
+
114
+ msg = "Enter api_hash: "
115
+ self.opt_cred.telethon_api_hash = self.cb.put(
116
+ (
117
+ "ask_str",
118
+ None,
119
+ {"question": msg, "initialvalue": self.opt_cred.telethon_api_hash},
120
+ )
121
+ )
122
+ if self.opt_cred.telethon_api_hash == "":
123
+ return False
124
+ return True
125
+
126
+ async def start_async(
127
+ self, check_auth_only: bool = False
128
+ ) -> Tuple[bool, Optional[TelegramClient], int, str, str]:
129
+ if self.opt_cred.telethon_api_id != 0 and self.opt_cred.telethon_api_hash != "":
130
+ success, client, api_id, api_hash, msg = await self.signin_async(
131
+ try_sign_in=False
132
+ )
133
+ if check_auth_only:
134
+ client.disconnect()
135
+ if success is True:
136
+ return success, client, api_id, api_hash, msg
137
+
138
+ self.guide()
139
+ cred_valid = self.get_api_info()
140
+ if cred_valid:
141
+ success, client, api_id, api_hash, msg = await self.signin_async()
142
+ if check_auth_only:
143
+ client.disconnect()
144
+ return success, client, api_id, api_hash, msg
145
+ else:
146
+ return False, None, 0, "", FAIL_MSG
147
+
148
+ def start(
149
+ self, check_auth_only: bool = False
150
+ ) -> Tuple[bool, Optional[TelegramClient], int, str, str]:
151
+ return anyio.run(self.start_async, check_auth_only)