sticker-convert 2.8.12__py3-none-any.whl → 2.17.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 (124) hide show
  1. sticker_convert/__main__.py +24 -24
  2. sticker_convert/auth/__init__.py +0 -0
  3. sticker_convert/auth/auth_base.py +19 -0
  4. sticker_convert/auth/auth_discord.py +149 -0
  5. sticker_convert/{utils/auth/get_kakao_auth.py → auth/auth_kakao_android_login.py} +331 -300
  6. sticker_convert/auth/auth_kakao_desktop_login.py +327 -0
  7. sticker_convert/auth/auth_kakao_desktop_memdump.py +281 -0
  8. sticker_convert/{utils/auth/get_line_auth.py → auth/auth_line.py} +98 -80
  9. sticker_convert/auth/auth_signal.py +139 -0
  10. sticker_convert/auth/auth_telethon.py +161 -0
  11. sticker_convert/auth/auth_viber.py +250 -0
  12. sticker_convert/auth/telegram_api.py +736 -0
  13. sticker_convert/cli.py +623 -509
  14. sticker_convert/converter.py +1093 -962
  15. sticker_convert/definitions.py +11 -0
  16. sticker_convert/downloaders/download_band.py +111 -0
  17. sticker_convert/downloaders/download_base.py +171 -130
  18. sticker_convert/downloaders/download_discord.py +92 -0
  19. sticker_convert/downloaders/download_kakao.py +417 -255
  20. sticker_convert/downloaders/download_line.py +484 -472
  21. sticker_convert/downloaders/download_ogq.py +80 -0
  22. sticker_convert/downloaders/download_signal.py +108 -92
  23. sticker_convert/downloaders/download_telegram.py +56 -130
  24. sticker_convert/downloaders/download_viber.py +121 -95
  25. sticker_convert/gui.py +788 -795
  26. sticker_convert/gui_components/frames/comp_frame.py +180 -165
  27. sticker_convert/gui_components/frames/config_frame.py +156 -113
  28. sticker_convert/gui_components/frames/control_frame.py +32 -30
  29. sticker_convert/gui_components/frames/cred_frame.py +232 -162
  30. sticker_convert/gui_components/frames/input_frame.py +139 -137
  31. sticker_convert/gui_components/frames/output_frame.py +112 -110
  32. sticker_convert/gui_components/frames/right_clicker.py +25 -23
  33. sticker_convert/gui_components/windows/advanced_compression_window.py +757 -715
  34. sticker_convert/gui_components/windows/base_window.py +7 -2
  35. sticker_convert/gui_components/windows/discord_get_auth_window.py +79 -0
  36. sticker_convert/gui_components/windows/kakao_get_auth_window.py +511 -186
  37. sticker_convert/gui_components/windows/line_get_auth_window.py +94 -102
  38. sticker_convert/gui_components/windows/signal_get_auth_window.py +84 -135
  39. sticker_convert/gui_components/windows/viber_get_auth_window.py +168 -0
  40. sticker_convert/ios-message-stickers-template/.github/FUNDING.yml +3 -3
  41. sticker_convert/ios-message-stickers-template/.gitignore +0 -0
  42. sticker_convert/ios-message-stickers-template/README.md +10 -10
  43. sticker_convert/ios-message-stickers-template/stickers/Info.plist +43 -43
  44. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Info.plist +31 -31
  45. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Contents.json +6 -6
  46. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Contents.json +20 -20
  47. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 1.sticker/Contents.json +9 -9
  48. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 1.sticker/Sticker 1.png +0 -0
  49. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 2.sticker/Contents.json +9 -9
  50. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 2.sticker/Sticker 2.png +0 -0
  51. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 3.sticker/Contents.json +9 -9
  52. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 3.sticker/Sticker 3.png +0 -0
  53. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/App-Store-1024x1024pt.png +0 -0
  54. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Contents.json +91 -91
  55. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-App-Store-1024x768pt.png +0 -0
  56. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPad-67x50pt@2x.png +0 -0
  57. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPad-Pro-74x55pt@2x.png +0 -0
  58. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPhone-60x45pt@2x.png +0 -0
  59. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages-iPhone-60x45pt@3x.png +0 -0
  60. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages27x20pt@2x.png +0 -0
  61. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages27x20pt@3x.png +0 -0
  62. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages32x24pt@2x.png +0 -0
  63. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Messages32x24pt@3x.png +0 -0
  64. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/iPad-Settings-29pt@2x.png +0 -0
  65. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/iPhone-Settings-29pt@3x.png +0 -0
  66. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/iPhone-settings-29pt@2x.png +0 -0
  67. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.pbxproj +364 -364
  68. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -7
  69. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -8
  70. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/xcuserdata/niklaspeterson.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  71. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/xcuserdata/niklaspeterson.xcuserdatad/xcschemes/xcschememanagement.plist +14 -14
  72. sticker_convert/job.py +279 -179
  73. sticker_convert/job_option.py +15 -2
  74. sticker_convert/locales/en_US/LC_MESSAGES/base.mo +0 -0
  75. sticker_convert/locales/ja_JP/LC_MESSAGES/base.mo +0 -0
  76. sticker_convert/locales/zh_CN/LC_MESSAGES/base.mo +0 -0
  77. sticker_convert/locales/zh_TW/LC_MESSAGES/base.mo +0 -0
  78. sticker_convert/py.typed +0 -0
  79. sticker_convert/resources/NotoColorEmoji.ttf +0 -0
  80. sticker_convert/resources/compression.json +220 -16
  81. sticker_convert/resources/emoji.json +527 -77
  82. sticker_convert/resources/help.ja_JP.json +88 -0
  83. sticker_convert/resources/help.json +24 -10
  84. sticker_convert/resources/help.zh_CN.json +88 -0
  85. sticker_convert/resources/help.zh_TW.json +88 -0
  86. sticker_convert/resources/input.ja_JP.json +74 -0
  87. sticker_convert/resources/input.json +121 -71
  88. sticker_convert/resources/input.zh_CN.json +74 -0
  89. sticker_convert/resources/input.zh_TW.json +74 -0
  90. sticker_convert/resources/memdump_linux.sh +25 -0
  91. sticker_convert/resources/memdump_windows.ps1 +8 -0
  92. sticker_convert/resources/output.ja_JP.json +38 -0
  93. sticker_convert/resources/output.json +24 -0
  94. sticker_convert/resources/output.zh_CN.json +38 -0
  95. sticker_convert/resources/output.zh_TW.json +38 -0
  96. sticker_convert/uploaders/compress_wastickers.py +186 -156
  97. sticker_convert/uploaders/upload_base.py +44 -35
  98. sticker_convert/uploaders/upload_signal.py +218 -173
  99. sticker_convert/uploaders/upload_telegram.py +353 -388
  100. sticker_convert/uploaders/upload_viber.py +178 -0
  101. sticker_convert/uploaders/xcode_imessage.py +295 -285
  102. sticker_convert/utils/callback.py +238 -6
  103. sticker_convert/utils/chrome_remotedebug.py +219 -0
  104. sticker_convert/utils/chromiums/linux.py +52 -0
  105. sticker_convert/utils/chromiums/osx.py +68 -0
  106. sticker_convert/utils/chromiums/windows.py +45 -0
  107. sticker_convert/utils/emoji.py +28 -0
  108. sticker_convert/utils/files/json_resources_loader.py +24 -19
  109. sticker_convert/utils/files/metadata_handler.py +8 -7
  110. sticker_convert/utils/files/run_bin.py +1 -1
  111. sticker_convert/utils/media/codec_info.py +99 -67
  112. sticker_convert/utils/media/format_verify.py +33 -20
  113. sticker_convert/utils/process.py +231 -0
  114. sticker_convert/utils/translate.py +108 -0
  115. sticker_convert/utils/url_detect.py +40 -33
  116. sticker_convert/version.py +1 -1
  117. {sticker_convert-2.8.12.dist-info → sticker_convert-2.17.0.0.dist-info}/METADATA +189 -96
  118. sticker_convert-2.17.0.0.dist-info/RECORD +138 -0
  119. {sticker_convert-2.8.12.dist-info → sticker_convert-2.17.0.0.dist-info}/WHEEL +1 -1
  120. sticker_convert/utils/auth/get_signal_auth.py +0 -129
  121. sticker_convert-2.8.12.dist-info/RECORD +0 -101
  122. {sticker_convert-2.8.12.dist-info → sticker_convert-2.17.0.0.dist-info}/entry_points.txt +0 -0
  123. {sticker_convert-2.8.12.dist-info → sticker_convert-2.17.0.0.dist-info/licenses}/LICENSE +0 -0
  124. {sticker_convert-2.8.12.dist-info → sticker_convert-2.17.0.0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env python3
2
+ from functools import partial
3
+ from getpass import getpass
2
4
  from multiprocessing import Event, Manager
3
5
  from multiprocessing.managers import ListProxy, SyncManager
4
6
  from queue import Queue
@@ -19,6 +21,9 @@ if TYPE_CHECKING:
19
21
  ResponseListType = ListProxy[ResponseItemType] # type: ignore
20
22
  CbQueueType = Queue[CbQueueItemType] # type: ignore
21
23
  WorkQueueType = Queue[WorkQueueItemType] # type: ignore
24
+ from ttkbootstrap import Toplevel # type: ignore
25
+
26
+ from sticker_convert.gui import GUI # type: ignore
22
27
  else:
23
28
  ResultsListType = List[Any]
24
29
  ResponseListType = List[ResponseItemType]
@@ -51,12 +56,13 @@ class CallbackProtocol(Protocol):
51
56
  def put(self, i: Union[CbQueueItemType, str]) -> Any: ...
52
57
 
53
58
 
54
- class Callback(CallbackProtocol):
59
+ class CallbackCli(CallbackProtocol):
55
60
  def __init__(
56
61
  self,
57
62
  msg: Optional[Callable[..., None]] = None,
58
63
  bar: Optional[Callable[..., None]] = None,
59
64
  msg_block: Optional[Callable[..., None]] = None,
65
+ msg_dynamic: Optional[Callable[..., bool]] = None,
60
66
  ask_bool: Optional[Callable[..., bool]] = None,
61
67
  ask_str: Optional[Callable[..., str]] = None,
62
68
  silent: bool = False,
@@ -80,6 +86,11 @@ class Callback(CallbackProtocol):
80
86
  else:
81
87
  self.msg_block = self.cb_msg_block
82
88
 
89
+ if msg_dynamic:
90
+ self.msg_dynamic = msg_dynamic
91
+ else:
92
+ self.msg_dynamic = self.cb_msg_dynamic
93
+
83
94
  if ask_bool:
84
95
  self.ask_bool = ask_bool
85
96
  else:
@@ -136,6 +147,16 @@ class Callback(CallbackProtocol):
136
147
  if not self.no_confirm:
137
148
  input("Press Enter to continue...")
138
149
 
150
+ def cb_msg_dynamic(self, *args: Any) -> bool:
151
+ message = args[0]
152
+
153
+ if message is None:
154
+ print()
155
+ else:
156
+ print(message, end="\r", flush=True)
157
+
158
+ return True
159
+
139
160
  def cb_ask_bool(self, *args: Any, **kwargs: Any) -> bool:
140
161
  question = args[0]
141
162
 
@@ -158,17 +179,23 @@ class Callback(CallbackProtocol):
158
179
 
159
180
  def cb_ask_str(
160
181
  self,
161
- msg: Optional[str] = None,
182
+ question: Optional[str] = None,
162
183
  initialvalue: Optional[str] = None,
163
184
  cli_show_initialvalue: bool = True,
185
+ password: bool = False,
164
186
  ) -> str:
165
- self.msg(msg)
187
+ self.msg(question)
166
188
 
167
189
  hint = ""
168
190
  if cli_show_initialvalue and initialvalue:
169
191
  hint = f" [Default: {initialvalue}]"
170
192
 
171
- response = input(f"Enter your response and press enter{hint} > ")
193
+ if password is True:
194
+ if question is None:
195
+ question = "Password: "
196
+ response = getpass(question)
197
+ else:
198
+ response = input(f"Enter your response and press enter{hint} > ")
172
199
 
173
200
  if initialvalue and not response:
174
201
  response = initialvalue
@@ -179,7 +206,210 @@ class Callback(CallbackProtocol):
179
206
  if isinstance(i, tuple):
180
207
  action = i[0]
181
208
  if len(i) >= 2:
182
- args: Tuple[str, ...] = i[1] if i[1] else tuple()
209
+ args: Tuple[Any, ...] = i[1] if i[1] else tuple()
210
+ else:
211
+ args = tuple()
212
+ if len(i) >= 3:
213
+ kwargs: Dict[str, Any] = i[2] if i[2] else {}
214
+ else:
215
+ kwargs = {}
216
+ else:
217
+ action = i
218
+ args = tuple()
219
+ kwargs = {}
220
+
221
+ # Fake implementation for Queue.put()
222
+ if action is None:
223
+ return None
224
+ if action == "msg":
225
+ self.msg(*args, **kwargs)
226
+ elif action == "bar":
227
+ self.bar(**kwargs)
228
+ elif action == "update_bar":
229
+ self.bar(update_bar=1)
230
+ elif action == "msg_block":
231
+ return self.msg_block(*args, **kwargs)
232
+ elif action == "msg_dynamic":
233
+ return self.msg_dynamic(*args, **kwargs)
234
+ elif action == "ask_bool":
235
+ return self.ask_bool(*args, **kwargs)
236
+ elif action == "ask_str":
237
+ return self.ask_str(*args, **kwargs)
238
+ else:
239
+ self.msg(action)
240
+ return None
241
+
242
+
243
+ class CallbackGui(CallbackProtocol):
244
+ def __init__(self, gui: "GUI"):
245
+ from ttkbootstrap import Label, Progressbar, StringVar # type: ignore
246
+
247
+ from sticker_convert.gui_components.windows.base_window import BaseWindow
248
+ from sticker_convert.job import Job
249
+
250
+ self.gui = gui
251
+ self.job: Optional[Job] = None
252
+ self.msg_dynamic_window: Optional[BaseWindow] = None
253
+ self.message_var = StringVar(self.gui)
254
+ self.msg_dynamic_label: Optional[Label] = None
255
+ self.msg_dynamic_bar: Optional[Progressbar] = None
256
+
257
+ self.ask_str = self.cb_ask_str
258
+ self.ask_bool = self.cb_ask_bool
259
+ self.msg = self.cb_msg
260
+ self.msg_block = self.cb_msg_block
261
+ self.msg_dynamic = self.cb_msg_dynamic
262
+ self.bar = self.cb_bar
263
+
264
+ def cb_ask_str(
265
+ self,
266
+ question: str,
267
+ initialvalue: Optional[str] = None,
268
+ cli_show_initialvalue: bool = True,
269
+ password: bool = False,
270
+ parent: Optional[object] = None,
271
+ ) -> str:
272
+ from ttkbootstrap import Querybox # type: ignore
273
+
274
+ self.gui.action = partial(
275
+ Querybox.get_string, # type: ignore
276
+ question,
277
+ title="sticker-convert",
278
+ initialvalue=initialvalue,
279
+ parent=parent,
280
+ )
281
+ self.gui.event_generate("<<exec_in_main>>")
282
+ self.gui.response_event.wait()
283
+ self.gui.response_event.clear()
284
+
285
+ if self.gui.response is None:
286
+ return ""
287
+ elif isinstance(self.gui.response, str):
288
+ return self.gui.response
289
+ else:
290
+ raise RuntimeError(f"Invalid response in cb_ask_str: {self.gui.response}")
291
+
292
+ def cb_ask_bool(self, question: str, parent: Optional["Toplevel"] = None) -> bool:
293
+ from ttkbootstrap.dialogs import Messagebox # type: ignore
294
+
295
+ self.gui.action = partial(
296
+ Messagebox.yesno, # type: ignore
297
+ question,
298
+ title="sticker-convert",
299
+ parent=parent,
300
+ )
301
+ self.gui.event_generate("<<exec_in_main>>")
302
+ self.gui.response_event.wait()
303
+ self.gui.response_event.clear()
304
+
305
+ if self.gui.response == "Yes":
306
+ return True
307
+ return False
308
+
309
+ def cb_msg(self, *args: Any, **kwargs: Any) -> None:
310
+ self.gui.progress_frame.update_message_box(*args, **kwargs)
311
+
312
+ def cb_msg_block(
313
+ self,
314
+ message: Optional[str] = None,
315
+ parent: Optional[object] = None,
316
+ **_kwargs: Any,
317
+ ) -> Any:
318
+ from ttkbootstrap.dialogs import Messagebox # type: ignore
319
+
320
+ self.gui.action = partial(
321
+ Messagebox.show_info, # type: ignore
322
+ message,
323
+ title="sticker-convert",
324
+ parent=parent,
325
+ )
326
+ self.gui.event_generate("<<exec_in_main>>")
327
+ self.gui.response_event.wait()
328
+ self.gui.response_event.clear()
329
+
330
+ return self.gui.response
331
+
332
+ def cb_msg_dynamic(
333
+ self,
334
+ message: Optional[str] = None,
335
+ parent: Optional["Toplevel"] = None,
336
+ **_kwargs: Any,
337
+ ) -> bool:
338
+ from ttkbootstrap import Label, Progressbar # type: ignore
339
+
340
+ from sticker_convert.gui_components.gui_utils import GUIUtils
341
+ from sticker_convert.gui_components.windows.base_window import BaseWindow
342
+
343
+ self.gui.action = None
344
+ if self.msg_dynamic_window is None:
345
+ if message is not None:
346
+ self.msg_dynamic_window = BaseWindow(self.gui, parent)
347
+ self.msg_dynamic_window.wm_title("sticker-convert")
348
+ self.message_var.set(message)
349
+ self.msg_dynamic_label = Label(
350
+ self.msg_dynamic_window.scrollable_frame,
351
+ textvariable=self.message_var,
352
+ )
353
+ self.msg_dynamic_bar = Progressbar(
354
+ self.msg_dynamic_window.scrollable_frame,
355
+ orient="horizontal",
356
+ mode="indeterminate",
357
+ )
358
+ self.msg_dynamic_bar.start(50)
359
+ self.msg_dynamic_label.pack()
360
+ self.msg_dynamic_bar.pack(fill="x")
361
+ self.gui.action = partial(
362
+ GUIUtils.finalize_window, self.msg_dynamic_window
363
+ )
364
+ print(message, end="\r", flush=True)
365
+ ret = True
366
+ else:
367
+ print()
368
+ ret = False
369
+ elif self.msg_dynamic_window.winfo_exists() == 0:
370
+ self.msg_dynamic_window = None
371
+ print()
372
+ ret = False
373
+ else:
374
+ if message is None:
375
+
376
+ def _action():
377
+ assert self.msg_dynamic_window is not None
378
+ self.msg_dynamic_window.destroy()
379
+ self.msg_dynamic_window = None
380
+
381
+ self.gui.action = _action
382
+ print()
383
+ ret = False
384
+ else:
385
+ self.gui.action = partial(self.message_var.set, message)
386
+ print(message, end="\r", flush=True)
387
+ ret = True
388
+
389
+ if self.gui.action is not None:
390
+ self.gui.event_generate("<<exec_in_main>>")
391
+ self.gui.response_event.wait()
392
+ self.gui.response_event.clear()
393
+
394
+ return ret
395
+
396
+ def cb_bar(
397
+ self,
398
+ set_progress_mode: Optional[str] = None,
399
+ steps: int = 0,
400
+ update_bar: int = 0,
401
+ *args: Any,
402
+ **kwargs: Any,
403
+ ) -> None:
404
+ self.gui.progress_frame.update_progress_bar(
405
+ set_progress_mode, steps, update_bar, *args, **kwargs
406
+ )
407
+
408
+ def put(self, i: Union[CbQueueItemType, str]) -> Union[str, bool, None]:
409
+ if isinstance(i, tuple):
410
+ action = i[0]
411
+ if len(i) >= 2:
412
+ args: Tuple[Any, ...] = i[1] if i[1] else tuple()
183
413
  else:
184
414
  args = tuple()
185
415
  if len(i) >= 3:
@@ -202,10 +432,12 @@ class Callback(CallbackProtocol):
202
432
  self.bar(update_bar=1)
203
433
  elif action == "msg_block":
204
434
  return self.msg_block(*args, **kwargs)
435
+ elif action == "msg_dynamic":
436
+ return self.msg_dynamic(*args, **kwargs)
205
437
  elif action == "ask_bool":
206
438
  return self.ask_bool(*args, **kwargs)
207
439
  elif action == "ask_str":
208
- return self.ask_str(**kwargs)
440
+ return self.ask_str(*args, **kwargs)
209
441
  else:
210
442
  self.msg(action)
211
443
  return None
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env python3
2
+ import base64
3
+ import io
4
+ import json
5
+ import os
6
+ import platform
7
+ import shutil
8
+ import signal
9
+ import socket
10
+ import subprocess
11
+ import time
12
+ from typing import Any, Dict, List, Optional, Union, cast
13
+
14
+ import requests
15
+ import websocket
16
+ from PIL import Image
17
+
18
+ # References
19
+ # https://github.com/yeongbin-jo/python-chromedriver-autoinstaller/blob/master/chromedriver_autoinstaller/utils.py
20
+ # https://chromedevtools.github.io/devtools-protocol/
21
+
22
+
23
+ BROWSER_PREF = [
24
+ "chrome",
25
+ "chrome-canary",
26
+ "chromium",
27
+ "msedge",
28
+ "msedge-beta",
29
+ "msedge-dev",
30
+ "msedge-canary",
31
+ "brave",
32
+ "brave-beta",
33
+ "brave-nightly",
34
+ "opera",
35
+ "opera-beta",
36
+ "opera-developer",
37
+ ]
38
+
39
+
40
+ def get_free_port() -> int:
41
+ with socket.socket() as sock:
42
+ sock.bind(("", 0))
43
+ port = sock.getsockname()[1]
44
+ return port
45
+
46
+
47
+ class CRD:
48
+ def __init__(
49
+ self,
50
+ chrome_bin: str,
51
+ port: Optional[int] = None,
52
+ args: Optional[List[str]] = None,
53
+ ) -> None:
54
+ if port is None:
55
+ port = get_free_port()
56
+ self.port = port
57
+
58
+ launch_cmd: List[str] = []
59
+ if (
60
+ platform.system() == "Linux"
61
+ and os.environ.get("DISPLAY", False) is False
62
+ and shutil.which("xvfb-run")
63
+ ):
64
+ launch_cmd += ["xvfb-run", "--server-args='-screen 0, 1024x768x24'"]
65
+
66
+ launch_cmd += [
67
+ chrome_bin,
68
+ f"--remote-debugging-port={port}",
69
+ f"--remote-allow-origins=http://127.0.0.1:{port}",
70
+ ]
71
+ if args:
72
+ launch_cmd += args
73
+
74
+ # Adding --no-sandbox in Windows may cause Signal fail to launch
75
+ # https://github.com/laggykiller/sticker-convert/issues/274
76
+ if (
77
+ platform.system() != "Windows"
78
+ and "geteuid" in dir(os)
79
+ and os.geteuid() == 0
80
+ ) or os.path.isfile("/.dockerenv"):
81
+ launch_cmd.append("--no-sandbox")
82
+
83
+ self.chrome_proc = subprocess.Popen(launch_cmd)
84
+
85
+ @staticmethod
86
+ def get_chromium_path() -> Optional[str]:
87
+ if platform.system() == "Windows":
88
+ from sticker_convert.utils.chromiums.windows import get_chromium_path
89
+ elif platform.system() == "Darwin":
90
+ from sticker_convert.utils.chromiums.osx import get_chromium_path
91
+ else:
92
+ from sticker_convert.utils.chromiums.linux import get_chromium_path
93
+ return get_chromium_path()
94
+
95
+ def connect(self, target_id: int = 0) -> None:
96
+ self.cmd_id = 1
97
+ r = None
98
+ targets: List[Any] = []
99
+ for _ in range(30):
100
+ try:
101
+ r = requests.get(f"http://127.0.0.1:{self.port}/json")
102
+ targets = json.loads(r.text)
103
+ if len(targets) == 0:
104
+ time.sleep(1)
105
+ continue
106
+ break
107
+ except requests.exceptions.ConnectionError:
108
+ time.sleep(1)
109
+
110
+ if r is None:
111
+ raise RuntimeError("Cannot connect to chrome debugging port")
112
+
113
+ if len(targets) == 0:
114
+ raise RuntimeError("Cannot create websocket connection with debugger")
115
+
116
+ self.ws = websocket.create_connection( # type: ignore
117
+ targets[target_id]["webSocketDebuggerUrl"]
118
+ )
119
+
120
+ def send_cmd(self, command: Dict[Any, Any]) -> Union[str, bytes]:
121
+ if command.get("id") is None:
122
+ command["id"] = self.cmd_id
123
+ for _ in range(3):
124
+ try:
125
+ self.ws.send(json.dumps(command))
126
+ r = self.ws.recv()
127
+ self.cmd_id += 1
128
+ return r
129
+ except BrokenPipeError:
130
+ self.connect()
131
+
132
+ raise RuntimeError("Websocket keep disconnecting")
133
+
134
+ def exec_js(self, js: str, context_id: Optional[int] = None) -> Union[str, bytes]:
135
+ command: Dict[str, Any] = {
136
+ "id": self.cmd_id,
137
+ "method": "Runtime.evaluate",
138
+ "params": {"expression": js},
139
+ }
140
+ if context_id is not None:
141
+ command["params"]["contextId"] = context_id
142
+ return self.send_cmd(command)
143
+
144
+ def set_transparent_bg(self) -> Union[str, bytes]:
145
+ command: Dict[str, Any] = {
146
+ "id": self.cmd_id,
147
+ "method": "Emulation.setDefaultBackgroundColorOverride",
148
+ "params": {"color": {"r": 0, "g": 0, "b": 0, "a": 0}},
149
+ }
150
+ return self.send_cmd(command)
151
+
152
+ def screenshot(self, clip: Optional[Dict[str, int]] = None) -> Image.Image:
153
+ command: Dict[str, Any] = {
154
+ "id": self.cmd_id,
155
+ "method": "Page.captureScreenshot",
156
+ "params": {"captureBeyondViewport": True, "optimizeForSpeed": True},
157
+ }
158
+ if clip:
159
+ command["params"]["clip"] = clip
160
+ result = self.send_cmd(command)
161
+ return Image.open(
162
+ io.BytesIO(base64.b64decode(json.loads(result)["result"]["data"]))
163
+ )
164
+
165
+ def get_curr_url(self) -> str:
166
+ r = self.exec_js("window.location.href")
167
+ return cast(
168
+ str, json.loads(r).get("result", {}).get("result", {}).get("value", "")
169
+ )
170
+
171
+ def navigate(self, url: str) -> None:
172
+ command = {"id": self.cmd_id, "method": "Page.navigate", "params": {"url": url}}
173
+ self.send_cmd(command)
174
+
175
+ def open_html_str(self, html: str) -> None:
176
+ command: Dict[str, Any] = {
177
+ "id": self.cmd_id,
178
+ "method": "Page.navigate",
179
+ "params": {"url": "about:blank"},
180
+ }
181
+ result = cast(str, self.send_cmd(command))
182
+ frame_id = json.loads(result).get("result", {}).get("frameId", None)
183
+ if frame_id is None:
184
+ raise RuntimeError(f"Cannot navigate to about:blank ({result})")
185
+
186
+ self.exec_js('document.getElementsByTagName("html")[0].remove()')
187
+
188
+ command = {
189
+ "id": self.cmd_id,
190
+ "method": "Page.setDocumentContent",
191
+ "params": {"frameId": frame_id, "html": html},
192
+ }
193
+ self.send_cmd(command)
194
+
195
+ def runtime_enable(self) -> None:
196
+ command = {
197
+ "method": "Runtime.enable",
198
+ }
199
+ self.send_cmd(command)
200
+
201
+ def runtime_disable(self) -> None:
202
+ command = {
203
+ "method": "Runtime.disable",
204
+ }
205
+ self.send_cmd(command)
206
+
207
+ def reload(self) -> None:
208
+ command = {
209
+ "method": "Page.reload",
210
+ }
211
+ self.send_cmd(command)
212
+
213
+ def close(self) -> None:
214
+ command = {
215
+ "method": "Browser.close",
216
+ }
217
+ self.send_cmd(command)
218
+ self.ws.close()
219
+ os.kill(self.chrome_proc.pid, signal.SIGTERM)
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env python3
2
+ import configparser
3
+ import os
4
+ import re
5
+ from typing import Optional
6
+
7
+ # Adopted from https://github.com/roniemartinez/browsers/blob/master/browsers/linux.py
8
+
9
+ LINUX_DESKTOP_ENTRY_LIST = (
10
+ ("chrome", ("google-chrome",)),
11
+ ("chromium", ("chromium", "chromium_chromium")),
12
+ ("brave", ("brave-browser", "brave_brave")),
13
+ ("brave-beta", ("brave-browser-beta",)),
14
+ ("brave-nightly", ("brave-browser-nightly",)),
15
+ ("msedge", ("microsoft-edge",)),
16
+ ("opera", ("opera_opera",)),
17
+ ("opera-beta", ("opera-beta_opera-beta",)),
18
+ ("opera-developer", ("opera-developer_opera-developer",)),
19
+ ("vivaldi", ("vivaldi_vivaldi-stable",)),
20
+ )
21
+
22
+ # $XDG_DATA_HOME and $XDG_DATA_DIRS are not always set
23
+ XDG_DATA_LOCATIONS = (
24
+ "~/.local/share/applications",
25
+ "/usr/share/applications",
26
+ "/var/lib/snapd/desktop/applications",
27
+ )
28
+
29
+ VERSION_PATTERN = re.compile(
30
+ r"\b(\S+\.\S+)\b"
31
+ ) # simple pattern assuming all version strings have a dot on them
32
+
33
+
34
+ def get_chromium_path() -> Optional[str]:
35
+ for _, desktop_entries in LINUX_DESKTOP_ENTRY_LIST:
36
+ for application_dir in XDG_DATA_LOCATIONS:
37
+ for desktop_entry in desktop_entries:
38
+ path = os.path.join(application_dir, f"{desktop_entry}.desktop")
39
+
40
+ if not os.path.isfile(path):
41
+ continue
42
+
43
+ config = configparser.ConfigParser(interpolation=None)
44
+ config.read(path, encoding="utf-8")
45
+ executable_path = config.get("Desktop Entry", "Exec")
46
+
47
+ if executable_path.lower().endswith(" %u"):
48
+ executable_path = executable_path[:-3].strip()
49
+
50
+ return executable_path
51
+
52
+ return None
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env python3
2
+ import glob
3
+ import os
4
+ import plistlib
5
+ import subprocess
6
+ from typing import Optional
7
+
8
+ # Adopted from https://github.com/roniemartinez/browsers/blob/master/browsers/osx.py
9
+
10
+ OSX_CHROMIUMS_BUNDLE_LIST = (
11
+ # browser name, bundle ID, version string
12
+ ("chrome", "com.google.Chrome", "CFBundleShortVersionString"),
13
+ ("chrome-beta", "com.google.Chrome.beta", "CFBundleShortVersionString"),
14
+ ("chrome-canary", "com.google.Chrome.canary", "CFBundleShortVersionString"),
15
+ ("chrome-dev", "com.google.Chrome.dev", "CFBundleShortVersionString"),
16
+ ("chrome-test", "com.google.chrome.for.testing", "CFBundleShortVersionString"),
17
+ ("chromium", "org.chromium.Chromium", "CFBundleShortVersionString"),
18
+ ("msedge", "com.microsoft.edgemac", "CFBundleShortVersionString"),
19
+ ("msedge-beta", "com.microsoft.edgemac.Beta", "CFBundleShortVersionString"),
20
+ ("msedge-dev", "com.microsoft.edgemac.Dev", "CFBundleShortVersionString"),
21
+ ("msedge-canary", "com.microsoft.edgemac.Canary", "CFBundleShortVersionString"),
22
+ ("brave", "com.brave.Browser", "CFBundleVersion"),
23
+ ("brave-beta", "com.brave.Browser.beta", "CFBundleVersion"),
24
+ ("brave-dev", "com.brave.Browser.dev", "CFBundleVersion"),
25
+ ("brave-nightly", "com.brave.Browser.nightly", "CFBundleVersion"),
26
+ ("opera", "com.operasoftware.Opera", "CFBundleVersion"),
27
+ ("opera-beta", "com.operasoftware.OperaNext", "CFBundleVersion"),
28
+ ("opera-developer", "com.operasoftware.OperaDeveloper", "CFBundleVersion"),
29
+ ("opera-gx", "com.operasoftware.OperaGX", "CFBundleVersion"),
30
+ ("opera-neon", "com.opera.Neon", "CFBundleShortVersionString"),
31
+ ("duckduckgo", "com.duckduckgo.macos.browser", "CFBundleShortVersionString"),
32
+ ("epic", "com.hiddenreflex.Epic", "CFBundleShortVersionString"),
33
+ ("vivaldi", "com.vivaldi.Vivaldi", "CFBundleShortVersionString"),
34
+ ("yandex", "ru.yandex.desktop.yandex-browser", "CFBundleShortVersionString"),
35
+ )
36
+
37
+ OSX_CHROMIUMS_BUNDLE_DICT = {item[1]: item for item in OSX_CHROMIUMS_BUNDLE_LIST}
38
+
39
+
40
+ def get_chromium_path() -> Optional[str]:
41
+ for _, bundle_id, _ in OSX_CHROMIUMS_BUNDLE_LIST:
42
+ app_dirs = subprocess.getoutput(
43
+ f'mdfind "kMDItemCFBundleIdentifier == {bundle_id}"'
44
+ ).splitlines()
45
+ for app_dir in app_dirs:
46
+ plist_path = os.path.join(app_dir, "Contents/Info.plist")
47
+
48
+ with open(plist_path, "rb") as f:
49
+ plist = plistlib.load(f)
50
+ executable_name = plist["CFBundleExecutable"]
51
+ executable = os.path.join(app_dir, "Contents/MacOS", executable_name)
52
+ return executable
53
+
54
+ # Naively iterate /Applications folder in case the above check fails
55
+ for plist_path in glob.glob("/Applications/*.app/Contents/Info.plist"):
56
+ with open(plist_path, "rb") as f:
57
+ plist = plistlib.load(f)
58
+ bundle_id = plist.get("CFBundleIdentifier")
59
+ if bundle_id not in OSX_CHROMIUMS_BUNDLE_DICT:
60
+ continue
61
+
62
+ app_dir = os.path.dirname(os.path.dirname(plist_path))
63
+ executable_name = plist["CFBundleExecutable"]
64
+ executable = os.path.join(app_dir, "Contents/MacOS", executable_name)
65
+
66
+ return executable
67
+
68
+ return None
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env python3
2
+ import os
3
+ from typing import Optional
4
+
5
+ # Not adopting approach of reading registry like https://github.com/roniemartinez/browsers
6
+ # As trigger antivirus
7
+
8
+ PF32 = os.environ.get("PROGRAMW6432")
9
+ PF64 = os.environ.get("PROGRAMFILES")
10
+ LAD = os.environ.get("LOCALAPPDATA")
11
+
12
+ WINDOWS_CHROMIUMS_PATHS = (
13
+ f"{PF64}\\Google\\Chrome\\Application\\chrome.exe", # chromium
14
+ f"{PF32}\\Google\\Chrome\\Application\\chrome.exe", # chromium
15
+ f"{LAD}\\Google\\Chrome SxS\\Application\\chrome.exe", # chromium-canary
16
+ f"{PF64}\\Opera\\opera.exe", # opera
17
+ f"{PF32}\\Opera\\opera.exe", # opera
18
+ f"{LAD}\\Programs\\Opera\\opera.exe", # opera
19
+ f"{LAD}\\Programs\\Opera\\launcher.exe", # opera
20
+ f"{LAD}\\Programs\\Opera Beta\\opera.exe", # opera-beta
21
+ f"{LAD}\\Programs\\Opera Beta\\launcher.exe", # opera-beta
22
+ f"{LAD}\\Programs\\Opera Developer\\opera.exe", # opera-developer
23
+ f"{LAD}\\Programs\\Opera Developer\\launcher.exe", # opera-developer
24
+ f"{PF64}\\Microsoft\\Edge\\Application\\msedge.exe", # msedge
25
+ f"{PF32}\\Microsoft\\Edge\\Application\\msedge.exe", # msedge
26
+ f"{PF64}\\Microsoft\\Edge Beta\\Application\\msedge.exe", # msedge-beta
27
+ f"{PF32}\\Microsoft\\Edge Beta\\Application\\msedge.exe", # msedge-beta
28
+ f"{PF64}\\Microsoft\\Edge Dev\\Application\\msedge.exe", # msedge-dev
29
+ f"{PF32}\\Microsoft\\Edge Dev\\Application\\msedge.exe", # msedge-dev
30
+ f"{LAD}\\Microsoft\\Edge SxS\\Application\\msedge.exe", # msedge-canary
31
+ f"{PF64}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe", # brave
32
+ f"{PF32}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe", # brave
33
+ f"{PF64}\\BraveSoftware\\Brave-Browser-Beta\\Application\\brave.exe", # brave-beta
34
+ f"{PF32}\\BraveSoftware\\Brave-Browser-Beta\\Application\\brave.exe", # brave-beta
35
+ f"{PF64}\\BraveSoftware\\Brave-Browser-Nightly\\Application\\brave.exe", # brave-nightly
36
+ f"{PF32}\\BraveSoftware\\Brave-Browser-Nightly\\Application\\brave.exe", # brave-nightly
37
+ )
38
+
39
+
40
+ def get_chromium_path() -> Optional[str]:
41
+ for path in WINDOWS_CHROMIUMS_PATHS:
42
+ if os.path.exists(path):
43
+ return path
44
+
45
+ return None