sticker-convert 2.13.3.0__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 (93) hide show
  1. sticker_convert/__main__.py +24 -27
  2. sticker_convert/auth/__init__.py +0 -0
  3. sticker_convert/auth/auth_base.py +19 -0
  4. sticker_convert/{utils/auth/get_discord_auth.py → auth/auth_discord.py} +149 -118
  5. sticker_convert/{utils/auth/get_kakao_auth.py → auth/auth_kakao_android_login.py} +331 -330
  6. sticker_convert/auth/auth_kakao_desktop_login.py +327 -0
  7. sticker_convert/{utils/auth/get_kakao_desktop_auth.py → auth/auth_kakao_desktop_memdump.py} +281 -263
  8. sticker_convert/{utils/auth/get_line_auth.py → auth/auth_line.py} +98 -80
  9. sticker_convert/{utils/auth/get_signal_auth.py → auth/auth_signal.py} +139 -135
  10. sticker_convert/auth/auth_telethon.py +161 -0
  11. sticker_convert/{utils/auth/get_viber_auth.py → auth/auth_viber.py} +250 -235
  12. sticker_convert/{utils/auth → auth}/telegram_api.py +736 -675
  13. sticker_convert/cli.py +623 -608
  14. sticker_convert/converter.py +1093 -1084
  15. sticker_convert/definitions.py +4 -0
  16. sticker_convert/downloaders/download_band.py +111 -110
  17. sticker_convert/downloaders/download_base.py +171 -166
  18. sticker_convert/downloaders/download_discord.py +92 -91
  19. sticker_convert/downloaders/download_kakao.py +417 -404
  20. sticker_convert/downloaders/download_line.py +484 -475
  21. sticker_convert/downloaders/download_ogq.py +80 -79
  22. sticker_convert/downloaders/download_signal.py +108 -105
  23. sticker_convert/downloaders/download_telegram.py +56 -55
  24. sticker_convert/downloaders/download_viber.py +121 -120
  25. sticker_convert/gui.py +788 -873
  26. sticker_convert/gui_components/frames/comp_frame.py +180 -166
  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 -233
  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 -757
  34. sticker_convert/gui_components/windows/base_window.py +7 -2
  35. sticker_convert/gui_components/windows/discord_get_auth_window.py +79 -82
  36. sticker_convert/gui_components/windows/kakao_get_auth_window.py +511 -321
  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 -89
  39. sticker_convert/gui_components/windows/viber_get_auth_window.py +168 -168
  40. sticker_convert/ios-message-stickers-template/.github/FUNDING.yml +3 -3
  41. sticker_convert/ios-message-stickers-template/README.md +10 -10
  42. sticker_convert/ios-message-stickers-template/stickers/Info.plist +43 -43
  43. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Info.plist +31 -31
  44. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Contents.json +6 -6
  45. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Contents.json +20 -20
  46. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 1.sticker/Contents.json +9 -9
  47. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 2.sticker/Contents.json +9 -9
  48. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/Sticker Pack.stickerpack/Sticker 3.sticker/Contents.json +9 -9
  49. sticker_convert/ios-message-stickers-template/stickers StickerPackExtension/Stickers.xcstickers/iMessage App Icon.stickersiconset/Contents.json +91 -91
  50. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.pbxproj +364 -364
  51. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -7
  52. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -8
  53. sticker_convert/ios-message-stickers-template/stickers.xcodeproj/xcuserdata/niklaspeterson.xcuserdatad/xcschemes/xcschememanagement.plist +14 -14
  54. sticker_convert/job.py +166 -130
  55. sticker_convert/job_option.py +1 -0
  56. sticker_convert/locales/en_US/LC_MESSAGES/base.mo +0 -0
  57. sticker_convert/locales/ja_JP/LC_MESSAGES/base.mo +0 -0
  58. sticker_convert/locales/zh_CN/LC_MESSAGES/base.mo +0 -0
  59. sticker_convert/locales/zh_TW/LC_MESSAGES/base.mo +0 -0
  60. sticker_convert/py.typed +0 -0
  61. sticker_convert/resources/NotoColorEmoji.ttf +0 -0
  62. sticker_convert/resources/help.ja_JP.json +88 -0
  63. sticker_convert/resources/help.json +10 -7
  64. sticker_convert/resources/help.zh_CN.json +88 -0
  65. sticker_convert/resources/help.zh_TW.json +88 -0
  66. sticker_convert/resources/input.ja_JP.json +74 -0
  67. sticker_convert/resources/input.json +121 -121
  68. sticker_convert/resources/input.zh_CN.json +74 -0
  69. sticker_convert/resources/input.zh_TW.json +74 -0
  70. sticker_convert/resources/output.ja_JP.json +38 -0
  71. sticker_convert/resources/output.zh_CN.json +38 -0
  72. sticker_convert/resources/output.zh_TW.json +38 -0
  73. sticker_convert/uploaders/compress_wastickers.py +186 -177
  74. sticker_convert/uploaders/upload_base.py +44 -35
  75. sticker_convert/uploaders/upload_signal.py +218 -203
  76. sticker_convert/uploaders/upload_telegram.py +353 -338
  77. sticker_convert/uploaders/upload_viber.py +178 -169
  78. sticker_convert/uploaders/xcode_imessage.py +295 -286
  79. sticker_convert/utils/callback.py +238 -6
  80. sticker_convert/utils/emoji.py +16 -4
  81. sticker_convert/utils/files/json_resources_loader.py +24 -19
  82. sticker_convert/utils/files/metadata_handler.py +3 -3
  83. sticker_convert/utils/translate.py +108 -0
  84. sticker_convert/utils/url_detect.py +40 -37
  85. sticker_convert/version.py +1 -1
  86. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.17.0.0.dist-info}/METADATA +89 -74
  87. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.17.0.0.dist-info}/RECORD +91 -74
  88. sticker_convert/utils/auth/telethon_setup.py +0 -97
  89. sticker_convert/utils/singletons.py +0 -18
  90. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.17.0.0.dist-info}/WHEEL +0 -0
  91. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.17.0.0.dist-info}/entry_points.txt +0 -0
  92. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.17.0.0.dist-info}/licenses/LICENSE +0 -0
  93. {sticker_convert-2.13.3.0.dist-info → sticker_convert-2.17.0.0.dist-info}/top_level.txt +0 -0
@@ -1,404 +1,417 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import itertools
5
- import json
6
- from pathlib import Path
7
- from typing import Any, Dict, List, Optional, Tuple
8
- from urllib.parse import urlparse
9
-
10
- import requests
11
-
12
- from sticker_convert.downloaders.download_base import DownloadBase
13
- from sticker_convert.job_option import CredOption, InputOption
14
- from sticker_convert.utils.callback import CallbackProtocol, CallbackReturn
15
- from sticker_convert.utils.files.metadata_handler import MetadataHandler
16
- from sticker_convert.utils.media.decrypt_kakao import DecryptKakao
17
-
18
-
19
- class MetadataKakao:
20
- @staticmethod
21
- def share_link_to_public_link(share_link: str) -> str:
22
- # Share link redirect to preview link if use desktop headers
23
- headers_desktop = {"User-Agent": "Chrome"}
24
- r = requests.get(share_link, headers=headers_desktop, allow_redirects=True)
25
- return r.url
26
-
27
- @staticmethod
28
- def get_item_code_from_hash(hash: str, auth_token: str) -> Optional[str]:
29
- headers = {
30
- "Authorization": auth_token,
31
- }
32
-
33
- data = {"hashedItemCode": hash}
34
-
35
- response = requests.post(
36
- "https://talk-pilsner.kakao.com/emoticon/api/store/v3/item-code-by-hash",
37
- headers=headers,
38
- data=data,
39
- )
40
-
41
- if response.status_code != 200:
42
- return None
43
-
44
- response_json = json.loads(response.text)
45
- item_code = response_json["itemCode"]
46
-
47
- return item_code
48
-
49
- @staticmethod
50
- def get_item_code_from_search(
51
- pack_title: str, search_term: str, by_author: bool, auth_token: str
52
- ) -> str:
53
- headers = {
54
- "Authorization": auth_token,
55
- }
56
-
57
- data = {"query": search_term}
58
-
59
- response = requests.post(
60
- "https://talk-pilsner.kakao.com/emoticon/item_store/instant_search",
61
- headers=headers,
62
- data=data,
63
- )
64
-
65
- if response.status_code != 200:
66
- return "auth_error"
67
-
68
- def check_pack_match(pack_info: Dict[str, Any]) -> bool:
69
- share_link = pack_info["itemMetaInfo"]["shareData"]["linkUrl"]
70
- public_url = MetadataKakao.share_link_to_public_link(share_link)
71
- if pack_title == urlparse(public_url).path.split("/")[-1]:
72
- return True
73
- return False
74
-
75
- response_json = json.loads(response.text)
76
- for emoticon in response_json["emoticons"]:
77
- item_code = emoticon["item_code"]
78
- pack_info = MetadataKakao.get_pack_info_authed(item_code, auth_token)
79
- if pack_info is None:
80
- continue
81
- if check_pack_match(pack_info):
82
- return item_code
83
- if by_author:
84
- cid = pack_info["itemMetaInfo"]["itemMetaData"]["cid"]
85
- for item_code in MetadataKakao.get_items_by_creator(cid, auth_token):
86
- pack_info = MetadataKakao.get_pack_info_authed(
87
- item_code, auth_token
88
- )
89
- if pack_info is None:
90
- continue
91
- if check_pack_match(pack_info):
92
- return item_code
93
-
94
- return "code_not_found"
95
-
96
- @staticmethod
97
- def get_items_by_creator(cid: str, auth_token: str) -> List[str]:
98
- headers = {"Authorization": auth_token}
99
-
100
- params = {
101
- "itemSort": "NEW",
102
- "offset": "0",
103
- "size": "30",
104
- }
105
-
106
- response = requests.get(
107
- f"https://talk-pilsner.kakao.com/emoticon/api/store/v3/creators/{cid}",
108
- headers=headers,
109
- params=params,
110
- )
111
-
112
- return [i["item_id"] for i in json.loads(response.text)["items"]]
113
-
114
- @staticmethod
115
- def get_pack_info_unauthed(
116
- pack_title: str,
117
- ) -> Optional[dict[str, Any]]:
118
- pack_meta_r = requests.get(f"https://e.kakao.com/api/v1/items/t/{pack_title}")
119
-
120
- if pack_meta_r.status_code == 200:
121
- pack_meta = json.loads(pack_meta_r.text)
122
- else:
123
- return None
124
-
125
- return pack_meta
126
-
127
- @staticmethod
128
- def get_pack_info_authed(
129
- item_code: str, auth_token: str
130
- ) -> Optional[dict[str, Any]]:
131
- headers = {
132
- "Authorization": auth_token,
133
- "Talk-Agent": "android/10.8.1",
134
- "Talk-Language": "en",
135
- "User-Agent": "okhttp/4.10.0",
136
- }
137
-
138
- response = requests.post(
139
- f"https://talk-pilsner.kakao.com/emoticon/api/store/v3/items/{item_code}",
140
- headers=headers,
141
- )
142
-
143
- if response.status_code != 200:
144
- return None
145
-
146
- response_json = json.loads(response.text)
147
-
148
- return response_json
149
-
150
-
151
- class DownloadKakao(DownloadBase):
152
- def __init__(self, *args: Any, **kwargs: Any) -> None:
153
- super().__init__(*args, **kwargs)
154
- self.pack_title: Optional[str] = None
155
- self.author: Optional[str] = None
156
- self.auth_token: Optional[str] = None
157
-
158
- self.pack_info_unauthed: Optional[dict[str, Any]] = None
159
- self.pack_info_authed: Optional[dict[str, Any]] = None
160
-
161
- def download_stickers_kakao(self) -> Tuple[int, int]:
162
- self.auth_token = None
163
- if self.opt_cred:
164
- self.auth_token = self.opt_cred.kakao_auth_token
165
-
166
- if urlparse(self.url).netloc == "emoticon.kakao.com":
167
- item_code = None
168
- if self.auth_token is not None:
169
- hash = urlparse(self.url).path.split("/")[-1]
170
- item_code = MetadataKakao.get_item_code_from_hash(hash, self.auth_token)
171
-
172
- public_url = MetadataKakao.share_link_to_public_link(self.url)
173
- self.pack_title = urlparse(public_url).path.split("/")[-1]
174
- pack_info_unauthed = MetadataKakao.get_pack_info_unauthed(self.pack_title)
175
- if pack_info_unauthed is None:
176
- self.cb.put(
177
- "Download failed: Cannot download metadata for sticker pack"
178
- )
179
- return 0, 0
180
-
181
- self.author = pack_info_unauthed["result"]["artist"]
182
- thumbnail_urls = pack_info_unauthed["result"]["thumbnailUrls"]
183
-
184
- if item_code is None:
185
- if self.auth_token is None:
186
- self.cb.put(
187
- "Warning: Downloading animated sticker requires auth_token"
188
- )
189
- else:
190
- self.cb.put(
191
- "Warning: auth_token invalid, cannot download animated sticker"
192
- )
193
- self.cb.put("Downloading static stickers...")
194
- return self.download_static(thumbnail_urls)
195
- else:
196
- return self.download_animated(item_code)
197
-
198
- if self.url.isnumeric():
199
- self.pack_title = None
200
- if self.auth_token:
201
- self.pack_info_authed = MetadataKakao.get_pack_info_authed(
202
- self.url, self.auth_token
203
- )
204
- if self.pack_info_authed:
205
- self.pack_title = self.pack_info_authed["itemUnitInfo"][0]["title"]
206
- else:
207
- self.cb.put("Warning: Cannot get pack_title with auth_token.")
208
- self.cb.put(
209
- "Is auth_token invalid / expired? Try to regenerate it."
210
- )
211
- self.cb.put("Continuing without getting pack_title")
212
-
213
- return self.download_animated(self.url)
214
-
215
- if urlparse(self.url).netloc == "e.kakao.com":
216
- self.pack_title = urlparse(self.url).path.split("/")[-1]
217
- self.pack_info_unauthed = MetadataKakao.get_pack_info_unauthed(
218
- self.pack_title
219
- )
220
-
221
- if not self.pack_info_unauthed:
222
- self.cb.put(
223
- "Download failed: Cannot download metadata for sticker pack"
224
- )
225
- return 0, 0
226
-
227
- self.author = self.pack_info_unauthed["result"]["artist"]
228
- title_ko = self.pack_info_unauthed["result"]["title"]
229
- thumbnail_urls = self.pack_info_unauthed["result"]["thumbnailUrls"]
230
-
231
- if self.auth_token:
232
- item_code = MetadataKakao.get_item_code_from_search(
233
- self.pack_title, title_ko, False, self.auth_token
234
- )
235
- if item_code == "auth_error":
236
- msg = "Warning: Cannot get item code.\n"
237
- msg += "Is auth_token invalid / expired? Try to regenerate it.\n"
238
- msg += "Continue to download static stickers instead?"
239
- elif item_code == "code_not_found":
240
- self.cb.put(
241
- "Cannot get item code, trying to search by author name, this may take long time..."
242
- )
243
- self.cb.put(
244
- "Hint: Use share link instead to download more reliably"
245
- )
246
- if self.author is not None:
247
- item_code = MetadataKakao.get_item_code_from_search(
248
- self.pack_title, self.author, True, self.auth_token
249
- )
250
- if item_code == "code_not_found":
251
- msg = "Warning: Cannot get item code.\n"
252
- msg += "Please use share link instead.\n"
253
- msg += "Continue to download static stickers instead?"
254
- else:
255
- return self.download_animated(item_code)
256
- else:
257
- return self.download_animated(item_code)
258
-
259
- self.cb.put(("ask_bool", (msg,), None))
260
- if self.cb_return:
261
- response = self.cb_return.get_response()
262
- else:
263
- response = False
264
-
265
- if response is False:
266
- return 0, 0
267
-
268
- return self.download_static(thumbnail_urls)
269
-
270
- self.cb.put("Download failed: Unrecognized URL")
271
- return 0, 0
272
-
273
- def download_static(self, thumbnail_urls: str) -> Tuple[int, int]:
274
- headers = {"User-Agent": "Android"}
275
- MetadataHandler.set_metadata(
276
- self.out_dir, title=self.pack_title, author=self.author
277
- )
278
-
279
- targets: List[Tuple[str, Path]] = []
280
-
281
- for num, url in enumerate(thumbnail_urls):
282
- dest = Path(self.out_dir, str(num).zfill(3) + ".png")
283
- targets.append((url, dest))
284
-
285
- results = self.download_multiple_files(targets, headers=headers)
286
-
287
- return sum(results.values()), len(targets)
288
-
289
- def download_animated(self, item_code: str) -> Tuple[int, int]:
290
- MetadataHandler.set_metadata(
291
- self.out_dir, title=self.pack_title, author=self.author
292
- )
293
-
294
- play_exts = [".webp", ".gif", ".png", ""]
295
- play_types = ["emot", "emoji", ""] # emot = normal; emoji = mini
296
- play_path_format = None
297
- sound_exts = [".mp3", ""]
298
- sound_path_format = None
299
- stickers_count = 32 # https://emoticonstudio.kakao.com/pages/start
300
-
301
- headers = {"User-Agent": "Android"}
302
-
303
- if not self.pack_info_authed and self.auth_token:
304
- self.pack_info_authed = MetadataKakao.get_pack_info_authed(
305
- item_code, self.auth_token
306
- )
307
- if self.pack_info_authed:
308
- preview_data = self.pack_info_authed["itemUnitInfo"][0]["previewData"]
309
- play_path_format = preview_data["playPathFormat"]
310
- sound_path_format = preview_data["soundPathFormat"]
311
- stickers_count = preview_data["num"]
312
- else:
313
- if not self.pack_info_unauthed:
314
- public_url = None
315
- if urlparse(self.url).netloc == "emoticon.kakao.com":
316
- public_url = MetadataKakao.share_link_to_public_link(self.url)
317
- elif urlparse(self.url).netloc == "e.kakao.com":
318
- public_url = self.url
319
- if public_url:
320
- pack_title = urlparse(public_url).path.split("/")[-1]
321
- self.pack_info_unauthed = MetadataKakao.get_pack_info_unauthed(
322
- pack_title
323
- )
324
-
325
- if self.pack_info_unauthed:
326
- stickers_count = len(self.pack_info_unauthed["result"]["thumbnailUrls"])
327
-
328
- play_type = ""
329
- play_ext = ""
330
- if play_path_format is None:
331
- for play_type, play_ext in itertools.product(play_types, play_exts):
332
- r = requests.get(
333
- f"https://item.kakaocdn.net/dw/{item_code}.{play_type}_001{play_ext}",
334
- headers=headers,
335
- )
336
- if r.ok:
337
- break
338
- if play_ext == "":
339
- self.cb.put(f"Failed to determine extension of {item_code}")
340
- return 0, 0
341
- else:
342
- play_path_format = f"dw/{item_code}.{play_type}_0##{play_ext}"
343
- else:
344
- play_ext = "." + play_path_format.split(".")[-1]
345
-
346
- sound_ext = ""
347
- if sound_path_format is None:
348
- for sound_ext in sound_exts:
349
- r = requests.get(
350
- f"https://item.kakaocdn.net/dw/{item_code}.sound_001{sound_ext}",
351
- headers=headers,
352
- )
353
- if r.ok:
354
- break
355
- if sound_ext != "":
356
- sound_path_format = f"dw/{item_code}.sound_0##{sound_ext}"
357
- elif sound_path_format != "":
358
- sound_ext = "." + sound_path_format.split(".")[-1]
359
-
360
- assert play_path_format
361
- targets: list[tuple[str, Path]] = []
362
- for num in range(1, stickers_count + 1):
363
- play_url = "https://item.kakaocdn.net/" + play_path_format.replace(
364
- "##", str(num).zfill(2)
365
- )
366
- play_dl_path = Path(self.out_dir, str(num).zfill(3) + play_ext)
367
- targets.append((play_url, play_dl_path))
368
-
369
- if sound_path_format:
370
- sound_url = "https://item.kakaocdn.net/" + sound_path_format.replace(
371
- "##", str(num).zfill(2)
372
- )
373
- sound_dl_path = Path(self.out_dir, str(num).zfill(3) + sound_ext)
374
- targets.append((sound_url, sound_dl_path))
375
-
376
- results = self.download_multiple_files(targets, headers=headers)
377
-
378
- for target in targets:
379
- f_path = target[1]
380
- ext = Path(f_path).suffix
381
-
382
- if ext not in (".gif", ".webp"):
383
- continue
384
-
385
- with open(f_path, "rb") as f:
386
- data = f.read()
387
- data = DecryptKakao.xor_data(data)
388
- self.cb.put(f"Decrypted {f_path}")
389
- with open(f_path, "wb+") as f:
390
- f.write(data)
391
-
392
- self.cb.put(f"Finished getting {item_code}")
393
-
394
- return sum(results.values()), len(targets)
395
-
396
- @staticmethod
397
- def start(
398
- opt_input: InputOption,
399
- opt_cred: Optional[CredOption],
400
- cb: CallbackProtocol,
401
- cb_return: CallbackReturn,
402
- ) -> Tuple[int, int]:
403
- downloader = DownloadKakao(opt_input, opt_cred, cb, cb_return)
404
- return downloader.download_stickers_kakao()
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import itertools
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional, Tuple
8
+ from urllib.parse import urlparse
9
+
10
+ import requests
11
+
12
+ from sticker_convert.downloaders.download_base import DownloadBase
13
+ from sticker_convert.job_option import CredOption, InputOption
14
+ from sticker_convert.utils.callback import CallbackProtocol, CallbackReturn
15
+ from sticker_convert.utils.files.metadata_handler import MetadataHandler
16
+ from sticker_convert.utils.media.decrypt_kakao import DecryptKakao
17
+ from sticker_convert.utils.translate import I
18
+
19
+
20
+ class MetadataKakao:
21
+ @staticmethod
22
+ def share_link_to_public_link(share_link: str) -> str:
23
+ # Share link redirect to preview link if use desktop headers
24
+ headers_desktop = {"User-Agent": "Chrome"}
25
+ r = requests.get(share_link, headers=headers_desktop, allow_redirects=True)
26
+ return r.url
27
+
28
+ @staticmethod
29
+ def get_item_code_from_hash(hash: str, auth_token: str) -> Optional[str]:
30
+ headers = {
31
+ "Authorization": auth_token,
32
+ }
33
+
34
+ data = {"hashedItemCode": hash}
35
+
36
+ response = requests.post(
37
+ "https://talk-pilsner.kakao.com/emoticon/api/store/v3/item-code-by-hash",
38
+ headers=headers,
39
+ data=data,
40
+ )
41
+
42
+ if response.status_code != 200:
43
+ return None
44
+
45
+ response_json = json.loads(response.text)
46
+ item_code = response_json["itemCode"]
47
+
48
+ return item_code
49
+
50
+ @staticmethod
51
+ def get_item_code_from_search(
52
+ pack_title: str, search_term: str, by_author: bool, auth_token: str
53
+ ) -> str:
54
+ headers = {
55
+ "Authorization": auth_token,
56
+ }
57
+
58
+ data = {"query": search_term}
59
+
60
+ response = requests.post(
61
+ "https://talk-pilsner.kakao.com/emoticon/item_store/instant_search",
62
+ headers=headers,
63
+ data=data,
64
+ )
65
+
66
+ if response.status_code != 200:
67
+ return "auth_error"
68
+
69
+ def check_pack_match(pack_info: Dict[str, Any]) -> bool:
70
+ share_link = pack_info["itemMetaInfo"]["shareData"]["linkUrl"]
71
+ public_url = MetadataKakao.share_link_to_public_link(share_link)
72
+ if pack_title == urlparse(public_url).path.split("/")[-1]:
73
+ return True
74
+ return False
75
+
76
+ response_json = json.loads(response.text)
77
+ for emoticon in response_json["emoticons"]:
78
+ item_code = emoticon["item_code"]
79
+ pack_info = MetadataKakao.get_pack_info_authed(item_code, auth_token)
80
+ if pack_info is None:
81
+ continue
82
+ if check_pack_match(pack_info):
83
+ return item_code
84
+ if by_author:
85
+ cid = pack_info["itemMetaInfo"]["itemMetaData"]["cid"]
86
+ for item_code in MetadataKakao.get_items_by_creator(cid, auth_token):
87
+ pack_info = MetadataKakao.get_pack_info_authed(
88
+ item_code, auth_token
89
+ )
90
+ if pack_info is None:
91
+ continue
92
+ if check_pack_match(pack_info):
93
+ return item_code
94
+
95
+ return "code_not_found"
96
+
97
+ @staticmethod
98
+ def get_items_by_creator(cid: str, auth_token: str) -> List[str]:
99
+ headers = {"Authorization": auth_token}
100
+
101
+ params = {
102
+ "itemSort": "NEW",
103
+ "offset": "0",
104
+ "size": "30",
105
+ }
106
+
107
+ response = requests.get(
108
+ f"https://talk-pilsner.kakao.com/emoticon/api/store/v3/creators/{cid}",
109
+ headers=headers,
110
+ params=params,
111
+ )
112
+
113
+ return [i["item_id"] for i in json.loads(response.text)["items"]]
114
+
115
+ @staticmethod
116
+ def get_pack_info_unauthed(
117
+ pack_title: str,
118
+ ) -> Optional[dict[str, Any]]:
119
+ pack_meta_r = requests.get(f"https://e.kakao.com/api/v1/items/t/{pack_title}")
120
+
121
+ if pack_meta_r.status_code == 200:
122
+ pack_meta = json.loads(pack_meta_r.text)
123
+ else:
124
+ return None
125
+
126
+ return pack_meta
127
+
128
+ @staticmethod
129
+ def get_pack_info_authed(
130
+ item_code: str, auth_token: str
131
+ ) -> Optional[dict[str, Any]]:
132
+ headers = {
133
+ "Authorization": auth_token,
134
+ "Talk-Agent": "android/10.8.1",
135
+ "Talk-Language": "en",
136
+ "User-Agent": "okhttp/4.10.0",
137
+ }
138
+
139
+ response = requests.post(
140
+ f"https://talk-pilsner.kakao.com/emoticon/api/store/v3/items/{item_code}",
141
+ headers=headers,
142
+ )
143
+
144
+ if response.status_code != 200:
145
+ return None
146
+
147
+ response_json = json.loads(response.text)
148
+
149
+ return response_json
150
+
151
+
152
+ class DownloadKakao(DownloadBase):
153
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
154
+ self.MSG_AUTH_ERROR = I(
155
+ "Warning: Cannot get item code.\n"
156
+ "Is auth_token invalid / expired? Try to regenerate it.\n"
157
+ "Continue to download static stickers instead?"
158
+ )
159
+
160
+ self.MSG_CODE_NOT_FOUND = I(
161
+ "Warning: Cannot get item code.\n"
162
+ "Please use share link instead.\n"
163
+ "Continue to download static stickers instead?"
164
+ )
165
+
166
+ super().__init__(*args, **kwargs)
167
+ self.pack_title: Optional[str] = None
168
+ self.author: Optional[str] = None
169
+ self.auth_token: Optional[str] = None
170
+
171
+ self.pack_info_unauthed: Optional[dict[str, Any]] = None
172
+ self.pack_info_authed: Optional[dict[str, Any]] = None
173
+
174
+ def download_stickers_kakao(self) -> Tuple[int, int]:
175
+ self.auth_token = None
176
+ if self.opt_cred:
177
+ self.auth_token = self.opt_cred.kakao_auth_token
178
+
179
+ if urlparse(self.url).netloc == "emoticon.kakao.com":
180
+ item_code = None
181
+ if self.auth_token is not None:
182
+ hash = urlparse(self.url).path.split("/")[-1]
183
+ item_code = MetadataKakao.get_item_code_from_hash(hash, self.auth_token)
184
+
185
+ public_url = MetadataKakao.share_link_to_public_link(self.url)
186
+ self.pack_title = urlparse(public_url).path.split("/")[-1]
187
+ pack_info_unauthed = MetadataKakao.get_pack_info_unauthed(self.pack_title)
188
+ if pack_info_unauthed is None:
189
+ self.cb.put(
190
+ I("Download failed: Cannot download metadata for sticker pack")
191
+ )
192
+ return 0, 0
193
+
194
+ self.author = pack_info_unauthed["result"]["artist"]
195
+ thumbnail_urls = pack_info_unauthed["result"]["thumbnailUrls"]
196
+
197
+ if item_code is None:
198
+ if self.auth_token is None:
199
+ self.cb.put(
200
+ I("Warning: Downloading animated sticker requires auth_token")
201
+ )
202
+ else:
203
+ self.cb.put(
204
+ I(
205
+ "Warning: auth_token invalid, cannot download animated sticker"
206
+ )
207
+ )
208
+ self.cb.put(I("Downloading static stickers..."))
209
+ return self.download_static(thumbnail_urls)
210
+ else:
211
+ return self.download_animated(item_code)
212
+
213
+ if self.url.isnumeric():
214
+ self.pack_title = None
215
+ if self.auth_token:
216
+ self.pack_info_authed = MetadataKakao.get_pack_info_authed(
217
+ self.url, self.auth_token
218
+ )
219
+ if self.pack_info_authed:
220
+ self.pack_title = self.pack_info_authed["itemUnitInfo"][0]["title"]
221
+ else:
222
+ self.cb.put(I("Warning: Cannot get pack_title with auth_token."))
223
+ self.cb.put(
224
+ I("Is auth_token invalid / expired? Try to regenerate it.")
225
+ )
226
+ self.cb.put(I("Continuing without getting pack_title"))
227
+
228
+ return self.download_animated(self.url)
229
+
230
+ if urlparse(self.url).netloc == "e.kakao.com":
231
+ self.pack_title = urlparse(self.url).path.split("/")[-1]
232
+ self.pack_info_unauthed = MetadataKakao.get_pack_info_unauthed(
233
+ self.pack_title
234
+ )
235
+
236
+ if not self.pack_info_unauthed:
237
+ self.cb.put(
238
+ I("Download failed: Cannot download metadata for sticker pack")
239
+ )
240
+ return 0, 0
241
+
242
+ self.author = self.pack_info_unauthed["result"]["artist"]
243
+ title_ko = self.pack_info_unauthed["result"]["title"]
244
+ thumbnail_urls = self.pack_info_unauthed["result"]["thumbnailUrls"]
245
+
246
+ if self.auth_token:
247
+ item_code = MetadataKakao.get_item_code_from_search(
248
+ self.pack_title, title_ko, False, self.auth_token
249
+ )
250
+ if item_code == "auth_error":
251
+ msg = self.MSG_AUTH_ERROR
252
+ elif item_code == "code_not_found":
253
+ self.cb.put(
254
+ I(
255
+ "Cannot get item code, trying to search by author name, this may take long time..."
256
+ )
257
+ )
258
+ self.cb.put(
259
+ I("Hint: Use share link instead to download more reliably")
260
+ )
261
+ if self.author is not None:
262
+ item_code = MetadataKakao.get_item_code_from_search(
263
+ self.pack_title, self.author, True, self.auth_token
264
+ )
265
+ if item_code == "code_not_found":
266
+ msg = self.MSG_CODE_NOT_FOUND
267
+ else:
268
+ return self.download_animated(item_code)
269
+ else:
270
+ return self.download_animated(item_code)
271
+
272
+ self.cb.put(("ask_bool", (msg,), None))
273
+ if self.cb_return:
274
+ response = self.cb_return.get_response()
275
+ else:
276
+ response = False
277
+
278
+ if response is False:
279
+ return 0, 0
280
+
281
+ return self.download_static(thumbnail_urls)
282
+
283
+ self.cb.put(I("Download failed: Unrecognized URL"))
284
+ return 0, 0
285
+
286
+ def download_static(self, thumbnail_urls: str) -> Tuple[int, int]:
287
+ headers = {"User-Agent": "Android"}
288
+ MetadataHandler.set_metadata(
289
+ self.out_dir, title=self.pack_title, author=self.author
290
+ )
291
+
292
+ targets: List[Tuple[str, Path]] = []
293
+
294
+ for num, url in enumerate(thumbnail_urls):
295
+ dest = Path(self.out_dir, str(num).zfill(3) + ".png")
296
+ targets.append((url, dest))
297
+
298
+ results = self.download_multiple_files(targets, headers=headers)
299
+
300
+ return sum(results.values()), len(targets)
301
+
302
+ def download_animated(self, item_code: str) -> Tuple[int, int]:
303
+ MetadataHandler.set_metadata(
304
+ self.out_dir, title=self.pack_title, author=self.author
305
+ )
306
+
307
+ play_exts = [".webp", ".gif", ".png", ""]
308
+ play_types = ["emot", "emoji", ""] # emot = normal; emoji = mini
309
+ play_path_format = None
310
+ sound_exts = [".mp3", ""]
311
+ sound_path_format = None
312
+ stickers_count = 32 # https://emoticonstudio.kakao.com/pages/start
313
+
314
+ headers = {"User-Agent": "Android"}
315
+
316
+ if not self.pack_info_authed and self.auth_token:
317
+ self.pack_info_authed = MetadataKakao.get_pack_info_authed(
318
+ item_code, self.auth_token
319
+ )
320
+ if self.pack_info_authed:
321
+ preview_data = self.pack_info_authed["itemUnitInfo"][0]["previewData"]
322
+ play_path_format = preview_data["playPathFormat"]
323
+ sound_path_format = preview_data["soundPathFormat"]
324
+ stickers_count = preview_data["num"]
325
+ else:
326
+ if not self.pack_info_unauthed:
327
+ public_url = None
328
+ if urlparse(self.url).netloc == "emoticon.kakao.com":
329
+ public_url = MetadataKakao.share_link_to_public_link(self.url)
330
+ elif urlparse(self.url).netloc == "e.kakao.com":
331
+ public_url = self.url
332
+ if public_url:
333
+ pack_title = urlparse(public_url).path.split("/")[-1]
334
+ self.pack_info_unauthed = MetadataKakao.get_pack_info_unauthed(
335
+ pack_title
336
+ )
337
+
338
+ if self.pack_info_unauthed:
339
+ stickers_count = len(self.pack_info_unauthed["result"]["thumbnailUrls"])
340
+
341
+ play_type = ""
342
+ play_ext = ""
343
+ if play_path_format is None:
344
+ for play_type, play_ext in itertools.product(play_types, play_exts):
345
+ r = requests.get(
346
+ f"https://item.kakaocdn.net/dw/{item_code}.{play_type}_001{play_ext}",
347
+ headers=headers,
348
+ )
349
+ if r.ok:
350
+ break
351
+ if play_ext == "":
352
+ self.cb.put(I("Failed to determine extension of {}").format(item_code))
353
+ return 0, 0
354
+ else:
355
+ play_path_format = f"dw/{item_code}.{play_type}_0##{play_ext}"
356
+ else:
357
+ play_ext = "." + play_path_format.split(".")[-1]
358
+
359
+ sound_ext = ""
360
+ if sound_path_format is None:
361
+ for sound_ext in sound_exts:
362
+ r = requests.get(
363
+ f"https://item.kakaocdn.net/dw/{item_code}.sound_001{sound_ext}",
364
+ headers=headers,
365
+ )
366
+ if r.ok:
367
+ break
368
+ if sound_ext != "":
369
+ sound_path_format = f"dw/{item_code}.sound_0##{sound_ext}"
370
+ elif sound_path_format != "":
371
+ sound_ext = "." + sound_path_format.split(".")[-1]
372
+
373
+ assert play_path_format
374
+ targets: list[tuple[str, Path]] = []
375
+ for num in range(1, stickers_count + 1):
376
+ play_url = "https://item.kakaocdn.net/" + play_path_format.replace(
377
+ "##", str(num).zfill(2)
378
+ )
379
+ play_dl_path = Path(self.out_dir, str(num).zfill(3) + play_ext)
380
+ targets.append((play_url, play_dl_path))
381
+
382
+ if sound_path_format:
383
+ sound_url = "https://item.kakaocdn.net/" + sound_path_format.replace(
384
+ "##", str(num).zfill(2)
385
+ )
386
+ sound_dl_path = Path(self.out_dir, str(num).zfill(3) + sound_ext)
387
+ targets.append((sound_url, sound_dl_path))
388
+
389
+ results = self.download_multiple_files(targets, headers=headers)
390
+
391
+ for target in targets:
392
+ f_path = target[1]
393
+ ext = Path(f_path).suffix
394
+
395
+ if ext not in (".gif", ".webp"):
396
+ continue
397
+
398
+ with open(f_path, "rb") as f:
399
+ data = f.read()
400
+ data = DecryptKakao.xor_data(data)
401
+ self.cb.put(I("Decrypted {}").format(f_path))
402
+ with open(f_path, "wb+") as f:
403
+ f.write(data)
404
+
405
+ self.cb.put(I("Finished getting {}").format(item_code))
406
+
407
+ return sum(results.values()), len(targets)
408
+
409
+ @staticmethod
410
+ def start(
411
+ opt_input: InputOption,
412
+ opt_cred: Optional[CredOption],
413
+ cb: CallbackProtocol,
414
+ cb_return: CallbackReturn,
415
+ ) -> Tuple[int, int]:
416
+ downloader = DownloadKakao(opt_input, opt_cred, cb, cb_return)
417
+ return downloader.download_stickers_kakao()