diff --git a/Dockerfile b/Dockerfile index 6683c9b..a3cf539 100755 --- a/Dockerfile +++ b/Dockerfile @@ -77,6 +77,11 @@ RUN wget -O- https://updates.signal.org/desktop/apt/keys.asc | gpg --dearmor > s apt update -y && \ apt install --no-install-recommends -y signal-desktop +# Install Viber Desktop +RUN curl -o /tmp/viber.deb -L https://download.cdn.viber.com/cdn/desktop/Linux/viber.deb && \ + apt install --no-install-recommends -y /tmp/viber.deb libgl1 libevent-2.1-7 libwebpdemux2 libxslt1.1 libxkbfile1 libegl1 libopengl0 libqt5gui5 && \ + rm /tmp/viber.deb + RUN apt purge -y curl wget gpg git && \ apt clean autoclean && \ apt autoremove --yes && \ diff --git a/README.md b/README.md index ccb3d96..37526dd 100644 --- a/README.md +++ b/README.md @@ -96,24 +96,27 @@ usage: sticker-convert.py [-h] [--version] [--no-confirm] [--no-progress] [--cus [--input-dir INPUT_DIR] [--download-auto DOWNLOAD_AUTO | --download-signal DOWNLOAD_SIGNAL | --download-telegram DOWNLOAD_TELEGRAM | --download-line DOWNLOAD_LINE | --download-kakao DOWNLOAD_KAKAO | --download-viber DOWNLOAD_VIBER] [--output-dir OUTPUT_DIR] [--author AUTHOR] [--title TITLE] - [--export-signal | --export-telegram | --export-telegram-emoji | --export-whatsapp | --export-imessage] + [--export-signal | --export-telegram | --export-telegram-emoji | --export-whatsapp | --export-viber | --export-imessage] [--no-compress] [--preset {auto,signal,telegram,telegram_emoji,whatsapp,line,kakao,viber,imessage_small,imessage_medium,imessage_large,custom}] [--steps STEPS] [--processes PROCESSES] [--fps-min FPS_MIN] [--fps-max FPS_MAX] [--fps-power FPS_POWER] [--res-min RES_MIN] [--res-max RES_MAX] [--res-w-min RES_W_MIN] - [--res-w-max RES_W_MAX] [--res-h-min RES_H_MIN] [--res-h-max RES_H_MAX] [--res-power RES_POWER] - [--quality-min QUALITY_MIN] [--quality-max QUALITY_MAX] [--quality-power QUALITY_POWER] - [--color-min COLOR_MIN] [--color-max COLOR_MAX] [--color-power COLOR_POWER] - [--duration-min DURATION_MIN] [--duration-max DURATION_MAX] [--padding-percent PADDING_PERCENT] - [--bg-color BG_COLOR] [--vid-size-max VID_SIZE_MAX] [--img-size-max IMG_SIZE_MAX] - [--vid-format VID_FORMAT] [--img-format IMG_FORMAT] [--fake-vid] [--scale-filter SCALE_FILTER] - [--quantize-method QUANTIZE_METHOD] [--cache-dir CACHE_DIR] [--default-emoji DEFAULT_EMOJI] - [--signal-uuid SIGNAL_UUID] [--signal-password SIGNAL_PASSWORD] [--signal-get-auth] + [--res-w-max RES_W_MAX] [--res-h-min RES_H_MIN] [--res-h-max RES_H_MAX] + [--res-power RES_POWER] [--quality-min QUALITY_MIN] [--quality-max QUALITY_MAX] + [--quality-power QUALITY_POWER] [--color-min COLOR_MIN] [--color-max COLOR_MAX] + [--color-power COLOR_POWER] [--duration-min DURATION_MIN] [--duration-max DURATION_MAX] + [--padding-percent PADDING_PERCENT] [--bg-color BG_COLOR] [--vid-size-max VID_SIZE_MAX] + [--img-size-max IMG_SIZE_MAX] [--vid-format VID_FORMAT] [--img-format IMG_FORMAT] + [--fake-vid] [--scale-filter SCALE_FILTER] [--quantize-method QUANTIZE_METHOD] + [--cache-dir CACHE_DIR] [--default-emoji DEFAULT_EMOJI] [--signal-uuid SIGNAL_UUID] + [--signal-password SIGNAL_PASSWORD] [--signal-get-auth] [--signal-data-dir SIGNAL_DATA_DIR] [--telegram-token TELEGRAM_TOKEN] - [--telegram-userid TELEGRAM_USERID] [--kakao-auth-token KAKAO_AUTH_TOKEN] [--kakao-get-auth] - [--kakao-username KAKAO_USERNAME] [--kakao-password KAKAO_PASSWORD] + [--telegram-userid TELEGRAM_USERID] [--kakao-auth-token KAKAO_AUTH_TOKEN] + [--kakao-get-auth] [--kakao-username KAKAO_USERNAME] [--kakao-password KAKAO_PASSWORD] [--kakao-country-code KAKAO_COUNTRY_CODE] [--kakao-phone-number KAKAO_PHONE_NUMBER] - [--line-get-auth] [--line-cookies LINE_COOKIES] [--save-cred SAVE_CRED] + [--line-get-auth] [--line-cookies LINE_COOKIES] [--viber-auth VIBER_AUTH] + [--viber-get-auth VIBER_GET_AUTH] [--viber-bin-path VIBER_BIN_PATH] + [--save-cred SAVE_CRED] CLI for stickers-convert @@ -163,6 +166,7 @@ Output options: --export-telegram-emoji Upload to Telegram (Custom emoji) --export-whatsapp Create a .wastickers file for uploading to WhatsApp + --export-viber Upload to Viber --export-imessage Create Xcode project for importing to iMessage Compression options: @@ -281,6 +285,14 @@ Credentials options: --line-get-auth Get Line cookies from browser, which is required to create custom message stickers. --line-cookies LINE_COOKIES Set Line cookies, which is required to create custom message stickers. + --viber-auth VIBER_AUTH + Set Viber authentication data. + Required for uploading Viber stickers. + --viber-get-auth VIBER_GET_AUTH + Generate Viber authentication data. + --viber-bin-path VIBER_BIN_PATH + Specify location of Viber Desktop application. + Useful for portable installation. --save-cred SAVE_CRED Save Signal and Telegram credentials. ``` @@ -453,5 +465,5 @@ See [docs/TODO.md](docs/TODO.md) - Banner generated from [GitHub Socialify](https://socialify.git.ci/) ## DISCLAIMER -- The author of this repo is NOT affiliated with Signal, Telegram, WhatsApp, Line, Kakao or Sticker Maker. +- The author of this repo is NOT affiliated with Signal, Telegram, WhatsApp, Line, Kakao, Viber, iMessage or Sticker Maker. - The author of this repo is NOT repsonsible for any legal consequences and loss incurred from using this repo. diff --git a/docs/guide_viber.md b/docs/guide_viber.md index 4068d89..bf9ba6e 100644 --- a/docs/guide_viber.md +++ b/docs/guide_viber.md @@ -1,7 +1,22 @@ +# Downloading Viber stickers sticker-convert supports downloading viber stickers from share link of sticker pack. -sticker-convert can also convert images to png with 490x490 for uploading to viber manually. -It should be noted that Viber is able to resize images for you, so it may not be necessary -to use sticker-convert for creating Viber sticker pack. +# Uploading Viber stickers +Viber authentication data required for uploading Viber stickers, which could be fetched +from Viber Desktop application automatically: +1. Install Viber Desktop +2. Login to Viber Desktop +3. In sticker-convert, press `Generate` button (In GUI) or `--viber-get-auth` (In CLI) +Notice: +- Viber Desktop would be closed, launched and closed again when getting auth data. +- It may take a minute to get auth data. +- On macOS, you need to disable SIP and will be asked for user password. +- For atypical installation of Viber Desktop, you may specify Viber Desktop application +location by using `--viber-bin-path`. +- `m_token`, `m_ts` and `member_id` are required. +- `m_ts` is the unix timestamp when `m_token` is generated. +- `m_token` expires after 13800 seconds (230 minutes). + +# References For more info, please refer to https://help.viber.com/hc/en-us/articles/9204828903837-Use-and-create-stickers-on-Rakuten-Viber \ No newline at end of file diff --git a/requirements-type.txt b/requirements-type.txt index 853f663..c46ef0e 100644 --- a/requirements-type.txt +++ b/requirements-type.txt @@ -1,4 +1,5 @@ types-beautifulsoup4 +types-psutil types-Pillow types-requests types-tqdm \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e635b2d..c62cdb6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,8 @@ numpy>=1.22.4 Pillow~=10.3.0 pyoxipng~=9.0.0 python-telegram-bot~=21.2 +psutil~=5.9.8 +PyMemoryEditor~=1.5.22 requests~=2.32.3 rlottie_python~=1.3.5 signalstickers-client-fork-laggykiller~=3.3.0.post2 diff --git a/src/sticker_convert/cli.py b/src/sticker_convert/cli.py index 5fa9484..47d5de5 100755 --- a/src/sticker_convert/cli.py +++ b/src/sticker_convert/cli.py @@ -17,6 +17,7 @@ from sticker_convert.utils.auth.get_kakao_auth import GetKakaoAuth from sticker_convert.utils.auth.get_line_auth import GetLineAuth from sticker_convert.utils.auth.get_signal_auth import GetSignalAuth +from sticker_convert.utils.auth.get_viber_auth import GetViberAuth from sticker_convert.utils.callback import Callback from sticker_convert.utils.files.json_manager import JsonManager from sticker_convert.utils.url_detect import UrlDetect @@ -452,6 +453,9 @@ def get_opt_cred(self, args: Namespace) -> CredOption: line_cookies=args.line_cookies if args.line_cookies else creds.get("line", {}).get("cookies"), + viber_auth=args.viber_auth + if args.viber_auth + else creds.get("viber", {}).get("auth"), ) if args.kakao_get_auth: @@ -501,6 +505,20 @@ def get_opt_cred(self, args: Namespace) -> CredOption: "Failed to get Line cookies. Have you logged in the web browser?" ) + if args.viber_get_auth: + get_viber_auth = GetViberAuth() + + viber_bin_path = None + if args.viber_bin_path: + viber_bin_path = args.viber_bin_path + + viber_auth, msg = get_viber_auth.get_cred(viber_bin_path) + + if viber_auth: + opt_cred.viber_auth = viber_auth + + self.cb.msg(msg) + if args.save_cred: creds_path = CONFIG_DIR / "creds.json" JsonManager.save_json(creds_path, opt_cred.to_dict()) diff --git a/src/sticker_convert/gui.py b/src/sticker_convert/gui.py index 7c52860..1323b7f 100755 --- a/src/sticker_convert/gui.py +++ b/src/sticker_convert/gui.py @@ -159,6 +159,8 @@ def declare_variables(self) -> None: self.kakao_country_code_var = StringVar(self) self.kakao_phone_number_var = StringVar(self) self.line_cookies_var = StringVar(self) + self.viber_auth_var = StringVar(self) + self.viber_bin_path_var = StringVar(self) # Config self.settings_save_cred_var = BooleanVar() @@ -385,6 +387,7 @@ def apply_creds(self) -> None: self.creds.get("kakao", {}).get("phone_number", "") ) self.line_cookies_var.set(self.creds.get("line", {}).get("cookies", "")) + self.viber_auth_var.set(self.creds.get("viber", {}).get("auth", "")) def get_input_name(self) -> str: return [ @@ -545,6 +548,7 @@ def get_opt_cred(self) -> CredOption: kakao_country_code=self.kakao_country_code_var.get(), kakao_phone_number=self.kakao_phone_number_var.get(), line_cookies=self.line_cookies_var.get(), + viber_auth=self.viber_auth_var.get(), ) def start_process(self) -> None: @@ -771,6 +775,11 @@ def highlight_fields(self) -> bool: else: self.cred_frame.telegram_userid_entry.config(bootstyle="default") # type: ignore + if output_option == "viber" and not self.viber_auth_var.get(): + self.cred_frame.viber_auth_entry.config(bootstyle="warning") # type: ignore + else: + self.cred_frame.viber_auth_entry.config(bootstyle="default") # type: ignore + if ( urlparse(url).netloc == "e.kakao.com" and not self.kakao_auth_token_var.get() diff --git a/src/sticker_convert/gui_components/frames/cred_frame.py b/src/sticker_convert/gui_components/frames/cred_frame.py index 8430c96..f60049b 100644 --- a/src/sticker_convert/gui_components/frames/cred_frame.py +++ b/src/sticker_convert/gui_components/frames/cred_frame.py @@ -8,6 +8,7 @@ from sticker_convert.gui_components.windows.kakao_get_auth_window import KakaoGetAuthWindow from sticker_convert.gui_components.windows.line_get_auth_window import LineGetAuthWindow from sticker_convert.gui_components.windows.signal_get_auth_window import SignalGetAuthWindow +from sticker_convert.gui_components.windows.viber_get_auth_window import ViberGetAuthWindow if TYPE_CHECKING: from sticker_convert.gui import GUI # type: ignore @@ -103,6 +104,20 @@ def __init__(self, gui: "GUI", *args: Any, **kwargs: Any) -> None: bootstyle="secondary", # type: ignore ) + self.viber_auth_lbl = Label( + self, text="Viber auth", width=18, justify="left", anchor="w" + ) + self.viber_auth_entry = Entry( + self, textvariable=self.gui.viber_auth_var, width=35 + ) + self.viber_auth_entry.bind("", RightClicker) + self.viber_get_auth_btn = Button( + self, + text="Generate", + command=self.cb_viber_get_auth, + bootstyle="secondary", # type: ignore + ) + self.help_btn = Button( self, text="Get help", @@ -133,6 +148,9 @@ def __init__(self, gui: "GUI", *args: Any, **kwargs: Any) -> None: self.line_cookies_lbl.grid(column=0, row=6, sticky="w", padx=3, pady=3) self.line_cookies_entry.grid(column=1, row=6, sticky="w", padx=3, pady=3) self.line_get_auth_btn.grid(column=2, row=6, sticky="e", padx=3, pady=3) + self.viber_auth_lbl.grid(column=0, row=7, sticky="w", padx=3, pady=3) + self.viber_auth_entry.grid(column=1, row=7, sticky="w", padx=3, pady=3) + self.viber_get_auth_btn.grid(column=2, row=7, sticky="e", padx=3, pady=3) self.help_btn.grid(column=2, row=8, sticky="e", padx=3, pady=3) def cb_cred_help(self, *_: Any) -> None: @@ -150,6 +168,9 @@ def cb_signal_get_auth(self, *_: Any) -> None: def cb_line_get_auth(self, *_: Any) -> None: LineGetAuthWindow(self.gui) + def cb_viber_get_auth(self, *_: Any) -> None: + ViberGetAuthWindow(self.gui) + def set_states(self, state: str) -> None: self.signal_uuid_entry.config(state=state) self.signal_password_entry.config(state=state) diff --git a/src/sticker_convert/gui_components/windows/viber_get_auth_window.py b/src/sticker_convert/gui_components/windows/viber_get_auth_window.py new file mode 100644 index 0000000..7393db9 --- /dev/null +++ b/src/sticker_convert/gui_components/windows/viber_get_auth_window.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +import platform +from functools import partial +from pathlib import Path +from subprocess import Popen +from tkinter import filedialog +from typing import Any + +from ttkbootstrap import Button, Entry, Frame, Label # type: ignore + +from sticker_convert.gui_components.gui_utils import GUIUtils +from sticker_convert.gui_components.windows.base_window import BaseWindow +from sticker_convert.utils.auth.get_viber_auth import GetViberAuth + + +class ViberGetAuthWindow(BaseWindow): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + self.title("Get Viber auth data") + + self.cb_msg_block_viber = partial(self.gui.cb_msg_block, parent=self) + self.cb_ask_str_viber = partial(self.gui.cb_ask_str, parent=self) + + self.frame_info = Frame(self.scrollable_frame) + self.frame_btns = Frame(self.scrollable_frame) + self.frame_config = Frame(self.scrollable_frame) + + self.frame_info.grid(column=0, row=0, sticky="news", padx=3, pady=3) + self.frame_btns.grid(column=0, row=1, sticky="news", padx=3, pady=3) + self.frame_config.grid(column=0, row=2, sticky="news", padx=3, pady=3) + + # Info frame + self.explanation_lbl0 = Label( + self.frame_info, + text="Please install Viber Desktop and login first.", + justify="left", + anchor="w", + ) + self.explanation_lbl1 = Label( + self.frame_info, + text="It may take a minute to get auth data.", + justify="left", + anchor="w", + ) + self.explanation_lbl2 = None + if platform.system() == "Darwin": + self.explanation_lbl2 = Label( + self.frame_info, + text="You need to disable SIP and will be asked for user password.", + justify="left", + anchor="w", + ) + + self.explanation_lbl0.grid(column=0, row=0, sticky="w", padx=3, pady=3) + self.explanation_lbl1.grid(column=0, row=1, sticky="w", padx=3, pady=3) + if self.explanation_lbl2 is not None: + self.explanation_lbl2.grid(column=0, row=2, sticky="w", padx=3, pady=3) + + # Start button frame + self.launch_btn = Button( + self.frame_btns, + text="Launch Viber Desktop", + command=self.cb_launch_viber, + bootstyle="secondary", # type: ignore + ) + + self.get_cred_btn = Button( + self.frame_btns, + text="Get auth data", + command=self.cb_get_cred, + bootstyle="default", # type: ignore + ) + + self.launch_btn.pack() + self.get_cred_btn.pack() + + # Config frame + self.setdir_lbl = Label( + self.frame_config, + text=self.gui.help["cred"]["viber_bin_path"], + justify="left", + anchor="w", + ) + + self.setdir_entry = Entry( + self.frame_config, + textvariable=self.gui.viber_bin_path_var, + width=32, + ) + self.setdir_btn = Button( + self.frame_config, + text="Choose", + command=self.cb_setdir, + width=8, + bootstyle="secondary", # type: ignore + ) + + self.setdir_lbl.grid(column=0, row=0, columnspan=2, sticky="w", padx=3, pady=3) + self.setdir_entry.grid(column=0, row=1, sticky="w", padx=3, pady=3) + self.setdir_btn.grid(column=1, row=1, sticky="e", padx=3, pady=3) + + GUIUtils.finalize_window(self) + + def cb_get_cred(self) -> None: + m = GetViberAuth() + + viber_bin_path = None + if self.gui.viber_bin_path_var.get(): + viber_bin_path = self.gui.viber_bin_path_var.get() + + viber_auth, msg = m.get_cred(viber_bin_path) + + if viber_auth: + if not self.gui.creds.get("viber"): + self.gui.creds["viber"] = {} + self.gui.creds["viber"]["auth"] = viber_auth + self.gui.viber_auth_var.set(viber_auth) + + self.gui.save_creds() + self.gui.highlight_fields() + + self.cb_msg_block_viber(msg) + + def cb_launch_viber(self) -> None: + m = GetViberAuth() + viber_bin_path = m.get_viber_desktop() + + if self.gui.viber_auth_var.get(): + viber_bin_path = self.gui.viber_auth_var.get() + + if viber_bin_path: + Popen([viber_bin_path]) + else: + self.cb_msg_block_viber("Error: Viber Desktop not installed.") + + def cb_setdir(self) -> None: + orig_input_dir = self.gui.viber_bin_path_var.get() + if not Path(orig_input_dir).is_dir(): + orig_input_dir = "" + input_dir = filedialog.askdirectory(initialdir=orig_input_dir) + if input_dir: + self.gui.viber_bin_path_var.set(input_dir) diff --git a/src/sticker_convert/job.py b/src/sticker_convert/job.py index fcf3b3b..bc7aa7a 100755 --- a/src/sticker_convert/job.py +++ b/src/sticker_convert/job.py @@ -21,6 +21,7 @@ from sticker_convert.uploaders.compress_wastickers import CompressWastickers from sticker_convert.uploaders.upload_signal import UploadSignal from sticker_convert.uploaders.upload_telegram import UploadTelegram +from sticker_convert.uploaders.upload_viber import UploadViber from sticker_convert.uploaders.xcode_imessage import XcodeImessage from sticker_convert.utils.callback import CallbackReturn, CbQueueType, ResultsListType, WorkQueueType from sticker_convert.utils.files.json_resources_loader import OUTPUT_JSON @@ -305,6 +306,10 @@ def verify_input(self) -> bool: error_msg += "[X] Uploading to signal requires uuid and password.\n" error_msg += save_to_local_tip + if self.opt_output.option == "viber" and not self.opt_cred.viber_auth: + error_msg += "[X] Uploading to Viber requires auth data.\n" + error_msg += save_to_local_tip + output_presets = OUTPUT_JSON input_option = self.opt_input.option @@ -656,6 +661,9 @@ def export(self) -> bool: if self.opt_output.option == "imessage": exporters.append(XcodeImessage.start) + if self.opt_output.option == "viber": + exporters.append(UploadViber.start) + self.executor.start_workers(processes=1) for exporter in exporters: diff --git a/src/sticker_convert/job_option.py b/src/sticker_convert/job_option.py index 7624d41..3bcc9d3 100755 --- a/src/sticker_convert/job_option.py +++ b/src/sticker_convert/job_option.py @@ -222,6 +222,7 @@ class CredOption(BaseOption): kakao_country_code: str = "" kakao_phone_number: str = "" line_cookies: str = "" + viber_auth: str = "" def to_dict(self) -> Dict[Any, Any]: return { @@ -235,4 +236,5 @@ def to_dict(self) -> Dict[Any, Any]: "phone_number": self.kakao_phone_number, }, "line": {"cookies": self.line_cookies}, + "viber": {"auth": self.viber_auth}, } diff --git a/src/sticker_convert/resources/compression.json b/src/sticker_convert/resources/compression.json index 7b80dbc..c737311 100755 --- a/src/sticker_convert/resources/compression.json +++ b/src/sticker_convert/resources/compression.json @@ -338,8 +338,8 @@ "vid": ".png" }, "fps": { - "min": 1, - "max": 1, + "min": 0, + "max": 0, "power": 1 }, "res": { diff --git a/src/sticker_convert/resources/help.json b/src/sticker_convert/resources/help.json index f8268bf..d8ef00e 100644 --- a/src/sticker_convert/resources/help.json +++ b/src/sticker_convert/resources/help.json @@ -69,6 +69,9 @@ "kakao_phone_number": "Set Kakao phone number (Phone number associated with your Kakao account)\nDo NOT enter country code\nExample: 7700900142\nUsed for send / receive verification code via SMS.\nRequired for generating Kakao auth_token.", "line_get_auth": "Get Line cookies from browser, which is required to create custom message stickers.", "line_cookies": "Set Line cookies, which is required to create custom message stickers.", + "viber_auth": "Set Viber authentication data.\nRequired for uploading Viber stickers.", + "viber_get_auth": "Generate Viber authentication data.", + "viber_bin_path": "Specify location of Viber Desktop application.\nUseful for portable installation.", "save_cred": "Save Signal and Telegram credentials." } } \ No newline at end of file diff --git a/src/sticker_convert/resources/output.json b/src/sticker_convert/resources/output.json index ff38639..a0e6d06 100644 --- a/src/sticker_convert/resources/output.json +++ b/src/sticker_convert/resources/output.json @@ -23,6 +23,14 @@ "author": false } }, + "viber": { + "full_name": "Upload to Viber", + "help": "Upload to Viber", + "metadata_requirements": { + "title": true, + "author": false + } + }, "whatsapp": { "full_name": "Compress to .wastickers (WhatsApp)", "help": "Create a .wastickers file for uploading to WhatsApp", diff --git a/src/sticker_convert/uploaders/upload_viber.py b/src/sticker_convert/uploaders/upload_viber.py new file mode 100755 index 0000000..cbffa50 --- /dev/null +++ b/src/sticker_convert/uploaders/upload_viber.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +import copy +import json +import shutil +import zipfile +from pathlib import Path +from typing import Any, Dict, List + +import requests + +from sticker_convert.converter import StickerConvert +from sticker_convert.job_option import CompOption, CredOption, OutputOption +from sticker_convert.uploaders.upload_base import UploadBase +from sticker_convert.utils.callback import CallbackProtocol, CallbackReturn +from sticker_convert.utils.files.cache_store import CacheStore +from sticker_convert.utils.files.metadata_handler import MetadataHandler +from sticker_convert.utils.files.sanitize_filename import sanitize_filename +from sticker_convert.utils.media.format_verify import FormatVerify + + +class UploadViber(UploadBase): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + self.base_spec.set_size_max(0) + self.base_spec.square = True + + self.png_spec = copy.deepcopy(self.base_spec) + self.png_spec.set_res_max(490) + self.png_spec.set_format((".png",)) + + self.png_cover_spec = copy.deepcopy(self.base_spec) + self.png_cover_spec.set_res_max(120) + self.png_spec.set_format((".png",)) + + self.opt_comp_merged = copy.deepcopy(self.opt_comp) + self.opt_comp_merged.merge(self.png_spec) + + def upload_stickers_viber(self) -> List[str]: + urls: List[str] = [] + + if not self.opt_cred.viber_auth: + self.cb.put("Viber auth required for uploading to viber") + return urls + + upload_data_base: Dict[str, str] = {} + for i in self.opt_cred.viber_auth.split(";"): + j = i.split(":") + upload_data_base[j[0]] = j[1] + + if upload_data_base.get("member_id") is None: + self.cb.put("Invalid Viber auth: Missing member_id") + return urls + if upload_data_base.get("m_token") is None: + self.cb.put("Invalid Viber auth: Missing m_token") + return urls + if upload_data_base.get("m_ts") is None: + self.cb.put("Invalid Viber auth: Missing m_ts") + return urls + + title, author, _ = MetadataHandler.get_metadata( + self.opt_output.dir, + title=self.opt_output.title, + author=self.opt_output.author, + ) + if title is None: + raise TypeError(f"title cannot be {title}") + if author is None: + author = "" + + packs = MetadataHandler.split_sticker_packs( + self.opt_output.dir, + title=title, + file_per_pack=24, + separate_image_anim=False, + ) + + cover_path_old = MetadataHandler.get_cover(self.opt_output.dir) + if cover_path_old: + cover_path = cover_path_old + else: + cover_path_old = MetadataHandler.get_stickers_present(self.opt_output.dir)[ + 0 + ] + cover_path = self.opt_output.dir / "cover.png" + + if not FormatVerify.check_file(cover_path_old, spec=self.png_cover_spec): + StickerConvert.convert( + cover_path_old, + cover_path, + self.opt_comp_merged, + self.cb, + self.cb_return, + ) + + for pack_title, stickers in packs.items(): + with CacheStore.get_cache_store(path=self.opt_comp.cache_dir) as tempdir: + for num, src in enumerate(stickers): + self.cb.put(f"Verifying {src} for uploading to Viber") + + dst = Path(tempdir, f"{str(num).zfill(2)}.png") + + if FormatVerify.check_file(src, spec=self.png_spec): + shutil.copy(src, dst) + else: + StickerConvert.convert( + Path(src), + Path(dst), + self.opt_comp_merged, + self.cb, + self.cb_return, + ) + + out_f = Path( + self.opt_output.dir, sanitize_filename(pack_title + ".zip") + ).as_posix() + + with zipfile.ZipFile(out_f, "w", zipfile.ZIP_DEFLATED) as zipf: + for file in Path(tempdir).iterdir(): + file_path = Path(tempdir, file.name) + zipf.write(file_path, arcname=file_path.name) + + upload_data = copy.deepcopy(upload_data_base) + upload_data["title"] = pack_title + upload_data["description"] = author + upload_data["shareable"] = "1" + + with open(out_f, "rb") as f, open(cover_path, "rb") as g: + r = requests.post( + "https://market.api.viber.com/2/users/custom-sticker-packs/create", + files={ + "file": ("upload.zip", f), + "file_icon": ("color_icon.png", g), + }, + data=upload_data, + ) + + if r.ok: + rjson = json.loads(r.text) + if rjson["status"] == 1: + pack_id = rjson["custom_sticker_pack"]["id"] + url = f"https://stickers.viber.com/pages/custom-sticker-packs/{pack_id}" + urls.append(url) + self.cb.put(f"Uploaded {pack_title}") + else: + self.cb.put( + f"Failed to upload {pack_title}: {r.status_code} {r.text}" + ) + if rjson["status"] == 103: + self.cb.put( + f"Viber auth data may have expired. Try to regenerate it?" + ) + else: + self.cb.put(f"Failed to upload {pack_title}: {r.status_code} {r.text}") + + return urls + + @staticmethod + def start( + opt_output: OutputOption, + opt_comp: CompOption, + opt_cred: CredOption, + cb: CallbackProtocol, + cb_return: CallbackReturn, + ) -> List[str]: + exporter = UploadViber(opt_output, opt_comp, opt_cred, cb, cb_return) + return exporter.upload_stickers_viber() diff --git a/src/sticker_convert/utils/auth/get_viber_auth.py b/src/sticker_convert/utils/auth/get_viber_auth.py new file mode 100755 index 0000000..76466f4 --- /dev/null +++ b/src/sticker_convert/utils/auth/get_viber_auth.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +import os +import platform +import shutil +import subprocess +import time +from pathlib import Path +from typing import Optional, Tuple, cast + +import psutil +from PyMemoryEditor import OpenProcess # type: ignore + +MSG_NO_BIN = """Viber Desktop not detected. +Download and install Viber Desktop, +then login to Viber Desktop and try again.""" + +MSG_NO_AUTH = """Viber Desktop installed, +but viber_auth not found. +Please login to Viber Desktop and try again.""" + +MSG_SIP_ENABLED = """You need to disable SIP: +1. Restart computer in Recovery mode +2. Launch Terminal from the Utilities menu +3. Run the command `csrutil disable` +4. Restart your computer""" + + +def killall(name: str) -> bool: + result = False + for proc in psutil.process_iter(): + if proc.name() == name: + proc.kill() + result = True + + return result + + +class GetViberAuth: + def get_auth(self, viber_bin_path: str) -> Tuple[Optional[str], str]: + member_id = None + m_token = None + m_ts = None + + viber_process_name = Path(viber_bin_path).name.replace(".AppImage", "") + + killed = killall(viber_process_name) + if killed: + time.sleep(5) + subprocess.Popen([viber_bin_path]) + time.sleep(10) + + with OpenProcess(process_name=viber_process_name) as process: + for address in process.search_by_value(str, 18, "X-Viber-Auth-Mid: "): # type: ignore + member_id_addr = cast(int, address) + 18 + member_id = process.read_process_memory(member_id_addr, str, 12) + break + if member_id is None: + return None, MSG_NO_AUTH + + for address in process.search_by_value(str, 20, "X-Viber-Auth-Token: "): # type: ignore + m_token_addr = cast(int, address) + 20 + m_token = process.read_process_memory(m_token_addr, str, 64) + break + if m_token is None: + return None, MSG_NO_AUTH + + for address in process.search_by_value(str, 24, "X-Viber-Auth-Timestamp: "): # type: ignore + m_ts_addr = cast(int, address) + 24 + m_ts = process.read_process_memory(m_ts_addr, str, 13) + break + if m_ts is None: + return None, MSG_NO_AUTH + + killall(viber_process_name) + + viber_auth = f"member_id:{member_id};m_token:{m_token};m_ts:{m_ts}" + msg = "Got viber_auth successfully:\n" + msg += f"{viber_auth=}\n" + + return viber_auth, msg + + def get_auth_darwin(self, viber_bin_path: str) -> Tuple[Optional[str], str]: + member_id = None + m_token = None + m_ts = None + + csrutil_status = subprocess.run( + ["csrutil", "status"], capture_output=True, text=True + ).stdout + + if "enabled" in csrutil_status: + return None, MSG_SIP_ENABLED + + killed = killall("Viber") + if killed: + time.sleep(5) + subprocess.run( + ["open", "-n", viber_bin_path], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + time.sleep(10) + + viber_pid = subprocess.run( + ["pgrep", "Viber"], capture_output=True, text=True + ).stdout.strip() + subprocess.run( + [ + "lldb", + "--attach-pid", + viber_pid, + "-o", + "process save-core /tmp/viber.dmp", + "-o", + "quit", + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + with open("/tmp/viber.dmp", "rb") as f: + s = f.read() + + os.remove("/tmp/viber.dmp") + killall("Viber") + + member_id_addr = s.find(b"X-Viber-Auth-Mid: ") + 18 + m_token_addr = s.find(b"X-Viber-Auth-Token: ") + 20 + m_ts_addr = s.find(b"X-Viber-Auth-Timestamp: ") + 24 + + if member_id_addr == -1 or m_token_addr == -1 or m_ts_addr == -1: + return None, MSG_NO_AUTH + + member_id = s[member_id_addr : member_id_addr + 12].decode(encoding="ascii") + m_token = s[m_token_addr : m_token_addr + 64].decode(encoding="ascii") + m_ts = s[m_ts_addr : m_ts_addr + 13].decode(encoding="ascii") + + viber_auth = f"member_id:{member_id};m_token:{m_token};m_ts:{m_ts}" + msg = "Got viber_auth successfully:\n" + msg += f"{viber_auth=}\n" + + return viber_auth, msg + + def get_viber_desktop(self) -> Optional[str]: + if platform.system() == "Windows": + viber_bin_path = os.path.expandvars("%localappdata%/Viber/Viber.exe") + elif platform.system() == "Darwin": + viber_bin_path = "/Applications/Viber.app" + else: + if os.path.isfile("/opt/viber/Viber"): + viber_bin_path = "/opt/viber/Viber" + else: + viber_which = shutil.which("Viber") + if viber_which is None: + viber_which = shutil.which("viber") + if viber_which is None: + viber_which = shutil.which("viber.AppImage") + if viber_which is None: + viber_bin_path = "viber" + else: + viber_bin_path = viber_which + + if Path(viber_bin_path).is_file(): + return viber_bin_path + + return None + + def get_cred( + self, + viber_bin_path: Optional[str] = None, + ) -> Tuple[Optional[str], str]: + if not viber_bin_path: + viber_bin_path = self.get_viber_desktop() + + if not viber_bin_path: + return None, MSG_NO_BIN + + if platform.system() == "Darwin": + return self.get_auth_darwin(viber_bin_path) + else: + return self.get_auth(viber_bin_path) diff --git a/src/sticker_convert/utils/files/metadata_handler.py b/src/sticker_convert/utils/files/metadata_handler.py index 13d5079..88dd98c 100755 --- a/src/sticker_convert/utils/files/metadata_handler.py +++ b/src/sticker_convert/utils/files/metadata_handler.py @@ -37,7 +37,7 @@ ) BLACKLIST_PREFIX = ("cover",) -BLACKLIST_SUFFIX = (".txt", ".m4a", ".wastickers", ".DS_Store", "._.DS_Store") +BLACKLIST_SUFFIX = (".txt", ".m4a", ".wastickers", ".zip", ".DS_Store", "._.DS_Store") XCODE_IMESSAGE_ICONSET = { "App-Store-1024x1024pt.png": (1024, 1024), diff --git a/sticker_convert_colab.ipynb b/sticker_convert_colab.ipynb index 6d25361..9d304ae 100644 --- a/sticker_convert_colab.ipynb +++ b/sticker_convert_colab.ipynb @@ -160,7 +160,7 @@ "\n", "#@markdown # Output\n", "#@markdown ### Output options\n", - "output_option = \"Upload to Signal\" #@param [\"Upload to Telegram\", \"Upload to Telegram (Custom emoji)\", \"Upload to Signal\", \"Compress to .wastickers (WhatsApp)\", \"Create Xcode project (iMessage)\", \"Save to Google Drive directory only\"]\n", + "output_option = \"Upload to Signal\" #@param [\"Upload to Signal\", \"Upload to Telegram\", \"Upload to Telegram (Custom emoji)\", \"Upload to Viber\", \"Compress to .wastickers (WhatsApp)\", \"Create Xcode project (iMessage)\", \"Save to Google Drive directory only\"]\n", "#@markdown ### Output directory\n", "output_dir = \"/content/drive/MyDrive/stickers_output\" #@param {type:\"string\"}\n", "#@markdown ### Title\n", @@ -190,6 +190,10 @@ "kakao_country_code = \"\" #@param {type:\"string\"}\n", "#@markdown ### Kakao phone number (For generating auth_token)\n", "kakao_phone_number = \"\" #@param {type:\"string\"}\n", + "#@markdown ### Line cookies\n", + "line_cookies = \"\" #@param {type:\"string\"}\n", + "#@markdown ### Viber auth data\n", + "viber_auth = \"\" #@param {type:\"string\"}\n", "\n", "params: List[Any] = []\n", "\n", @@ -239,6 +243,11 @@ " if signal_password == \"\":\n", " print(\"ERROR: Signal password required for uploading signal stickers\")\n", " exit()\n", + "elif output_option == \"Upload to Viber\":\n", + " params.append(\"--export-viber\")\n", + " if viber_auth == \"\":\n", + " print(\"ERROR: Viber auth data required for uploading Viber stickers\")\n", + " exit()\n", "elif output_option == \"Compress to .wastickers (WhatsApp)\":\n", " params.append(\"--export-whatsapp\")\n", "elif output_option == \"Create Xcode project (iMessage)\":\n", @@ -367,6 +376,12 @@ "if kakao_phone_number != \"\":\n", " params.append(\"--kakao-phone-number\")\n", " params.append(str(kakao_phone_number))\n", + "if line_cookies != \"\":\n", + " params.append(\"--line-cookies\")\n", + " params.append(str(line_cookies))\n", + "if viber_auth != \"\":\n", + " params.append(\"--viber-auth\")\n", + " params.append(str(viber_auth))\n", "\n", "cmds = [\"/content/sticker-convert\", \"--no-confirm\", \"--no-progress\"]\n", "\n", diff --git a/tests/common.py b/tests/common.py index 16241e2..12fb843 100644 --- a/tests/common.py +++ b/tests/common.py @@ -35,6 +35,7 @@ def get_python_path() -> str: TELEGRAM_USERID = CREDS_JSON_DICT.get("telegram", {}).get("userid") KAKAO_TOKEN = CREDS_JSON_DICT.get("kakao", {}).get("auth_token") LINE_COOKIES = CREDS_JSON_DICT.get("line", {}).get("cookies") + VIBER_AUTH = CREDS_JSON_DICT.get("viber", {}).get("auth") else: SIGNAL_UUID = os.environ.get("SIGNAL_UUID") # type: ignore SIGNAL_PASSWORD = os.environ.get("SIGNAL_PASSWORD") # type: ignore @@ -42,6 +43,7 @@ def get_python_path() -> str: TELEGRAM_USERID = os.environ.get("TELEGRAM_USERID") # type: ignore KAKAO_TOKEN = os.environ.get("KAKAO_TOKEN") # type: ignore LINE_COOKIES = os.environ.get("LINE_COOKIES") # type: ignore + VIBER_AUTH = os.environ.get("VIBER_AUTH") # type: ignore def run_cmd(cmd: List[str], **kwargs: Any) -> None: diff --git a/tests/test_export.py b/tests/test_export.py index 5188687..ee8d251 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -6,7 +6,7 @@ import pytest from _pytest._py.path import LocalPath -from tests.common import COMPRESSION_DICT, PYTHON_EXE, SAMPLE_DIR, SIGNAL_PASSWORD, SIGNAL_UUID, SRC_DIR, TELEGRAM_TOKEN, TELEGRAM_USERID, run_cmd +from tests.common import COMPRESSION_DICT, PYTHON_EXE, SAMPLE_DIR, SIGNAL_PASSWORD, SIGNAL_UUID, SRC_DIR, TELEGRAM_TOKEN, TELEGRAM_USERID, VIBER_AUTH, run_cmd os.chdir(Path(__file__).resolve().parent) sys.path.append("../src") @@ -129,6 +129,12 @@ def test_upload_telegram_emoji_with_upload(tmp_path: LocalPath) -> None: _run_sticker_convert(tmp_path, "telegram_emoji", None) +@pytest.mark.skipif(not TEST_UPLOAD, reason="TEST_UPLOAD not set") +@pytest.mark.skipif(not VIBER_AUTH, reason="No credentials") +def test_upload_viber_with_upload(tmp_path: LocalPath) -> None: + _run_sticker_convert(tmp_path, "viber", "viber") + + @pytest.mark.skipif( TELEGRAM_TOKEN is not None and TELEGRAM_USERID is not None, reason="With credentials", @@ -153,6 +159,14 @@ def test_upload_telegram_emoji(tmp_path: LocalPath) -> None: _run_sticker_convert(tmp_path, "telegram_emoji", None) +@pytest.mark.skipif( + VIBER_AUTH is not None, + reason="With credentials", +) +def test_export_viber(tmp_path: LocalPath) -> None: + _run_sticker_convert(tmp_path, "viber", None) + + def test_export_wastickers(tmp_path: LocalPath) -> None: _run_sticker_convert(tmp_path, "whatsapp", "whatsapp")