├── .gitignore ├── LICENSE ├── README.md ├── global_config ├── __init__.py └── environment_config.py ├── install_centos.sh ├── localization └── translator.py ├── log_helper ├── __init__.py └── msg_logger.py ├── main_run_bot.py ├── requirements.txt └── telegram_bot ├── __init__.py ├── bot_executor.py ├── func_helper.py ├── gif_downloader.py └── sticker_set_downloader.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .vscode/ 3 | 4 | .env/ 5 | .openssl/ 6 | .app_cache/ 7 | global_config/protected* 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ryan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GIFBot 2 | 3 | Here is the repo of [@GIFDownloader_bot](https://t.me/GIFDownloader_bot) which help exporting GIF, Sticker and Sticker Set from Telegram to local. 4 | 5 | To develop and run this bot, you may require the following basic environments. 6 | 7 | 1. A machine that can install `Python 3.6` and `FFmpeg`, for example `Linux (CentOS 7.4)`. 8 | 2. Check your security settings and ensure the port `8443` is opened if you want to setup webhook with your bot. 9 | 3. Own a bot and get the token from [@BotFather](https://t.me/BotFather). 10 | 4. Follow the installation tips. 11 | 12 | 13 | ## Installation 14 | 15 | This guidance requires `CentOS 7.4`. If not, you can try translate them to your system commands. 16 | 17 | **Notice**: before installation, don't forget to change to `root` user by calling `sudo su -`. 18 | 19 | ### 1. [Python3](https://www.python.org/downloads/) 20 | ```bash 21 | yum -y install epel-release 22 | yum -y install https://centos7.iuscommunity.org/ius-release.rpm 23 | yum -y install python36u 24 | yum -y install python36u-pip 25 | ``` 26 | 27 | Check if it is installed successfully, run: 28 | 29 | ```bash 30 | python3.6 -V 31 | pip3.6 -V 32 | ``` 33 | 34 | ### 2. [FFmpeg](https://www.ffmpeg.org/download.html) 35 | ```bash 36 | rpm –import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro 37 | rpm -Uvh http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-5.el7.nux.noarch.rpm 38 | yum -y install ffmpeg ffmpeg-devel 39 | ``` 40 | 41 | Check if it is installed successfully, run: 42 | 43 | ```bash 44 | ffmpeg -version 45 | ``` 46 | 47 | ### 3. Repository 48 | 49 | ``` 50 | git clone https://github.com/ryanfwy/gifbot.git 51 | ``` 52 | 53 | It is highly recommend you to install python `requirements.txt` under the virtual environment. 54 | 55 | ```bash 56 | cd gifbot 57 | 58 | python3 -m venv .env 59 | source .env/bin/activate 60 | pip3 install -r requirements.txt 61 | ``` 62 | 63 | ### 4. Configuration 64 | 65 | #### 1) Setup bot token 66 | 67 | It requires a file `global_config/protected_config.py` to place your bot token. 68 | 69 | ```bash 70 | touch global_config/protected_config.py 71 | echo "_telegrambot_token = 'YOUR_TOKEN'" > global_config/protected_config.py 72 | ``` 73 | 74 | And your bot's locations on `global_config/environment_config.py`. 75 | 76 | ```python 77 | _base_dir = 'YOUR_ABSOLUTE_BOT_LOCATION' 78 | _temp_dir = 'YOUR_ABSOLUTE_CACHE_LOCATION' 79 | ``` 80 | 81 | #### 2) Setup webhook (optional) 82 | 83 | If you want to run your bot using webhook, it requires a SSL certificate and your own domain. 84 | 85 | ```bash 86 | mkdir .openssl && cd .openssl 87 | openssl req -newkey rsa:2048 -sha256 -nodes -keyout private.key -x509 -days 3650 -out cert.pem && cd .. 88 | ``` 89 | 90 | #### 3) Setup entrance 91 | 92 | If you want to run your bot using webhook, you should change `YOUR_DOMAIN` on `main_run_bot.py`. 93 | 94 | ```python 95 | import os 96 | 97 | from global_config.environment_config import _base_dir 98 | from telegram_bot.bot_executor import BotExecutor 99 | 100 | 101 | if __name__ == '__main__': 102 | cert_path = os.path.join(_base_dir, '.openssl/cert.pem') 103 | key_path = os.path.join(_base_dir, '.openssl/private.key') 104 | webhook_url = 'https://YOUR_DOMAIN:8443/gifbot' 105 | BotExecutor(cert_path=cert_path, 106 | key_path=key_path, 107 | webhook_url=webhook_url).execute() 108 | ``` 109 | 110 | Otherwise, if you just simply debug and test the code, overwrite `main_run_bot.py` like this: 111 | 112 | ```python 113 | from telegram_bot.bot_executor import BotExecutor 114 | 115 | if __name__ == '__main__': 116 | BotExecutor().execute() 117 | ``` 118 | 119 | #### 4) Run 120 | ```bash 121 | python3 main_run_bot.py 122 | ``` 123 | 124 | ## Quick Installation 125 | 126 | Install on this way WON'T setup all the steps metioned above, such as webhook and bot entrance, but it can help you to install some basic environments quickly. 127 | 128 | **Notice**: `CentOS 7.4` required. 129 | 130 | ```bash 131 | bash install_centos.sh 132 | ``` 133 | 134 | ## Questions 135 | 136 | To `activate` and `deactivate` the vitural environment, run: 137 | 138 | ```bash 139 | # activate 140 | source .env/bin/activate 141 | 142 | # deactivate 143 | deactivate 144 | ``` 145 | 146 | ## License 147 | 148 | This repo has been licnesed by [MIT](https://github.com/ryanfwy/gifbot/blob/master/LICENSE). 149 | -------------------------------------------------------------------------------- /global_config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanfwy/gifbot/6f1eb29c856a5ec81ec5173536c7473bbfe18254/global_config/__init__.py -------------------------------------------------------------------------------- /global_config/environment_config.py: -------------------------------------------------------------------------------- 1 | '''''' 2 | 3 | _base_dir = 'YOUR_ABSOLUTE_LOCATION/gifbot' 4 | _temp_dir = 'YOUR_ABSOLUTE_LOCATION/temp' 5 | -------------------------------------------------------------------------------- /install_centos.sh: -------------------------------------------------------------------------------- 1 | echo ">>> 1. install Python 3.6" 2 | sudo yum -y install epel-release 3 | sudo yum -y install https://centos7.iuscommunity.org/ius-release.rpm 4 | sudo yum -y install python36u 5 | sudo yum -y install python36u-pip 6 | 7 | python3.6 -V 8 | pip3.6 -V 9 | 10 | echo ">>> 2. install FFmpeg" 11 | sudo rpm –import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro 12 | sudo rpm -Uvh http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-5.el7.nux.noarch.rpm 13 | sudo yum -y install ffmpeg ffmpeg-devel 14 | 15 | ffmpeg -version 16 | 17 | echo ">>> 3. setup repository" 18 | python3 -m venv .env 19 | source .env/bin/activate 20 | pip3 install -r requirements.txt 21 | 22 | echo ">>> 4. setup configuration" 23 | read -p "Input your bot token: " token 24 | touch global_config/protected_config.py 25 | echo "_telegrambot_token = '$token'" > global_config/protected_config.py 26 | 27 | read -p "Input your repo absolute location: " base_dir 28 | echo "_base_dir = '$base_dir'" > global_config/environment_config.py 29 | 30 | read -p "Input your cache absolute location: " temp_dir 31 | echo "_temp_dir = '$temp_dir'" >> global_config/environment_config.py 32 | 33 | echo ">>> 5. setup entrance" 34 | echo "One more step to be done, setup your bot entrance by yourself." 35 | -------------------------------------------------------------------------------- /localization/translator.py: -------------------------------------------------------------------------------- 1 | '''''' 2 | 3 | 4 | _translation = { 5 | 'zh': { 6 | 'start': 'Hi %(user)s,欢迎光临🎊\n\n使用 /help 查看使用方法。', 7 | 'help': '''*TG Downloader Bot 使用方法* 8 | 1. GIF 表情: 9 | ☛ 直接发送 `GIF 表情`。 10 | ☚ 打包的 `MP4` 和 `GIF` 文件。 11 | 12 | 2. 静态贴纸: 13 | ☛ 直接发送`静态贴纸`。 14 | ☚ `PNG` 文件。 15 | 16 | 3. 动态贴纸: 17 | ☛ 机器资源有限尚未支持。 18 | 19 | 4. 静态贴纸集: 20 | ☛ 直接发送`静态贴纸`,点击下方按钮「下载完整贴纸集」。 21 | ☛ 直接发送`静态贴纸集链接`,如 [EmmaWatsonStickers](https://t.me/addstickers/EmmaWatsonStickers)。 22 | ☚ 打包的 `PNG` 文件。 23 | 24 | ⚠︎ 下载贴纸集十分耗费资源,如非必要还请挑选后逐个下载。对此不胜感激。 25 | 26 | 请尽情使用 :)''', 27 | 'limit_exceed': '今日用量已超出 %(limit)s,请明天再试', 28 | 'kb_sticker_set': '下载完整贴纸集', 29 | 'unsupport': '资源有限尚未支持,请自行下载转换', 30 | 'zip_preparing': '正在准备文件,时间较长请稍等', 31 | 'zip_packing': '等待文件打包,时间较长请稍等', 32 | 'zip_timeout': '等待超时,请稍后重试', 33 | 'exec_error': '执行错误,请自行下载转换', 34 | 'file_size_exceed': '文件过大,请自行下载转换', 35 | }, 36 | 37 | 'en': { 38 | 'start': 'Hi %(user)s,Welcome🎊\n\nUse /help to find detail usage.', 39 | 'help': '''*TG Downloader Bot Usage* 40 | 1. GIF: 41 | ☛ Send `GIF` directly. 42 | ☚ File packed with `MP4` and `GIF`. 43 | 44 | 2. Static Sticker: 45 | ☛ Send `Sticker` directly. 46 | ☚ File of `PNG`. 47 | 48 | 3. Animated Sticker: 49 | ☛ Due to the resource limitation, it is not support to decode this type of sticker right now. 50 | 51 | 4. Static Sticker Set: 52 | ☛ Send `Sticker` first, then tap「Download Sticker Set」button. 53 | ☛ Send `Sticker Set Link` directly, for example [EmmaWatsonStickers](https://t.me/addstickers/EmmaWatsonStickers). 54 | ☚ File packed with a list of `PNGs`. 55 | 56 | ⚠︎ It is resource consuming to download the entire sticker set, PLEASE select and download what you want one by one if not necessary. I would deeply appreciate if you do so. 57 | 58 | Hope you enjoy it :)''', 59 | 'limit_exceed': 'Limit exceed %(limit)s today, try tomorrow', 60 | 'kb_sticker_set': 'Download Sticker Set', 61 | 'unsupport': 'Resource limited, please download and decode by yourself', 62 | 'zip_preparing': 'File preparing, hold on please', 63 | 'zip_packing': 'File packing, hold on please', 64 | 'zip_timeout': 'Timeout waiting, try again later', 65 | 'exec_error': 'Error executing, please download and decode by yourself', 66 | 'file_size_exceed': 'File too large, please download and decode by yourself', 67 | } 68 | } 69 | _translation_default = _translation['en'] 70 | 71 | 72 | def l10n(key, locale='en'): 73 | '''''' 74 | locale = locale or 'en' 75 | locale = locale[:2] if len(locale) > 2 else locale 76 | document = _translation.get(locale, _translation_default) 77 | return document.get(key, key) 78 | 79 | 80 | if __name__ == '__main__': 81 | pass 82 | -------------------------------------------------------------------------------- /log_helper/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanfwy/gifbot/6f1eb29c856a5ec81ec5173536c7473bbfe18254/log_helper/__init__.py -------------------------------------------------------------------------------- /log_helper/msg_logger.py: -------------------------------------------------------------------------------- 1 | '''''' 2 | 3 | import logging 4 | 5 | 6 | class MsgLogger(): 7 | def __init__(self, 8 | log_name='gifbot', 9 | log_file='.app_cache/telegram.log'): 10 | '''''' 11 | self.log_name = log_name 12 | self.log_file = log_file 13 | # self.fmt = '%(asctime)s - %(levelname)s - %(message)s' 14 | self.fmt = '[%(asctime)s] %(filename)s:%(lineno)d: %(levelname)s: %(message)s' 15 | self.formatter = logging.Formatter(fmt=self.fmt, datefmt=r'%Y-%m-%d %H:%M:%S') 16 | 17 | def get_logger(self): 18 | '''''' 19 | logger = logging.getLogger(self.log_name) 20 | logger.setLevel(logging.DEBUG) 21 | # console handler 22 | console_hdlr = logging.StreamHandler() 23 | console_hdlr.setLevel(logging.DEBUG) 24 | console_hdlr.setFormatter(self.formatter) 25 | # file handler 26 | file_hdlr = logging.FileHandler(self.log_file) 27 | file_hdlr.setLevel(logging.INFO) 28 | file_hdlr.setFormatter(self.formatter) 29 | # add handlers 30 | if not logger.handlers: 31 | logger.addHandler(console_hdlr) 32 | logger.addHandler(file_hdlr) 33 | return logger 34 | 35 | 36 | if __name__ == '__main__': 37 | pass 38 | -------------------------------------------------------------------------------- /main_run_bot.py: -------------------------------------------------------------------------------- 1 | '''''' 2 | 3 | import os 4 | 5 | from global_config.environment_config import _base_dir 6 | from telegram_bot.bot_executor import BotExecutor 7 | 8 | 9 | if __name__ == '__main__': 10 | cert_path = os.path.join(_base_dir, '.openssl/cert.pem') 11 | key_path = os.path.join(_base_dir, '.openssl/private.key') 12 | webhook_url = 'https://YOUR_DOMAIN:8443/gifbot' 13 | BotExecutor(cert_path=cert_path, 14 | key_path=key_path, 15 | webhook_url=webhook_url).execute() 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2019.11.28 2 | cffi==1.14.0 3 | chardet==3.0.4 4 | cryptography==2.8 5 | decorator==4.4.1 6 | dill==0.3.1.1 7 | future==0.18.2 8 | idna==2.8 9 | multiprocess==0.70.9 10 | numpy==1.18.1 11 | pandas==1.0.1 12 | pathos==0.2.5 13 | Pillow==7.0.0 14 | pox==0.2.7 15 | ppft==1.6.6.1 16 | pycparser==2.19 17 | python-dateutil==2.8.1 18 | python-telegram-bot==12.4.2 19 | pytz==2019.3 20 | requests==2.22.0 21 | six==1.14.0 22 | tornado==6.0.3 23 | urllib3==1.25.8 24 | -------------------------------------------------------------------------------- /telegram_bot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanfwy/gifbot/6f1eb29c856a5ec81ec5173536c7473bbfe18254/telegram_bot/__init__.py -------------------------------------------------------------------------------- /telegram_bot/bot_executor.py: -------------------------------------------------------------------------------- 1 | '''''' 2 | 3 | import os 4 | import re 5 | import time 6 | from telegram import File, InlineKeyboardButton, InlineKeyboardMarkup 7 | from telegram.ext import (CommandHandler, MessageHandler, CallbackQueryHandler, RegexHandler, 8 | Updater, Filters, PicklePersistence) 9 | from telegram.ext.dispatcher import run_async 10 | 11 | from global_config.protected_config import _telegrambot_token 12 | from global_config.environment_config import _base_dir, _temp_dir 13 | from telegram_bot.sticker_set_downloader import (download_sticker, download_sticker_set, 14 | download_sticker_animated_pack) 15 | from telegram_bot.gif_downloader import download_gif_pack 16 | from telegram_bot.func_helper import random_string 17 | from localization.translator import l10n 18 | from log_helper.msg_logger import MsgLogger 19 | 20 | 21 | STICKER_SET = 'sticker_set' 22 | LIMITATION = 100 * 1024 * 1024 # 100MB 23 | LIMITATION_STRING = str(round(LIMITATION/(1024*1024))) + 'MB' 24 | 25 | 26 | def is_usage_exceed(context, limit=LIMITATION): 27 | # check new day 28 | today = time.strftime('%Y-%m-%d', time.localtime()) 29 | user_dict = context.user_data 30 | 31 | # get usage 32 | if 'today_tag' not in user_dict or user_dict['today_tag'] != today: 33 | user_dict['today_tag'] = today 34 | user_dict['today_usage'] = 0 35 | elif 'today_usage' not in user_dict: 36 | user_dict['today_usage'] = 0 37 | 38 | usage = user_dict['today_usage'] 39 | return usage >= limit 40 | 41 | def set_usage(context, file_path=None): 42 | if not os.path.isfile(file_path): 43 | return -1 44 | 45 | # set usage 46 | file_size = os.path.getsize(file_path) 47 | user_dict = context.user_data 48 | user_dict['today_usage'] += file_size 49 | return user_dict['today_usage'] 50 | 51 | def check_usage_limit(func): 52 | def warpper(*args, **kwargs): 53 | _, update, context, *_ = args 54 | if is_usage_exceed(context): 55 | # language 56 | locale = update.effective_user.language_code 57 | message = l10n('limit_exceed', locale) % {'limit': LIMITATION_STRING} 58 | update.effective_message.reply_text(message) 59 | return None 60 | else: 61 | return func(*args, **kwargs) 62 | return warpper 63 | 64 | 65 | class BotExecutor(): 66 | def __init__(self, 67 | cert_path=None, 68 | key_path=None, 69 | webhook_url=None, 70 | persistence_file_path=os.path.join(_base_dir, '.app_cache/bot_data'), 71 | log_file_path=os.path.join(_base_dir, '.app_cache/app.log')): 72 | '''''' 73 | # telegram 74 | self.cert_path = cert_path 75 | self.key_path = key_path 76 | self.webhook_url = webhook_url 77 | 78 | # persistence 79 | _dir = os.path.dirname(persistence_file_path) 80 | os.path.isdir(_dir) or os.makedirs(_dir) 81 | self.persistence_file_path = persistence_file_path 82 | 83 | # log 84 | _dir = os.path.dirname(log_file_path) 85 | os.path.isdir(_dir) or os.makedirs(_dir) 86 | self.logger = MsgLogger(log_file=log_file_path).get_logger() 87 | 88 | self.gif_file_size_max = 1 * 1024 * 1024 # 1MB 89 | 90 | 91 | ''' helper functions ''' 92 | 93 | def base_send_zip(self, file_name, callback, callback_args, 94 | update=None, context=None, pack_name=None): 95 | if not update and not context: 96 | return -1 97 | 98 | # language 99 | locale = update.effective_user.language_code 100 | 101 | # parse update and context 102 | chat = update.effective_chat 103 | message = update.effective_message 104 | 105 | try: 106 | file_dir = os.path.join(_temp_dir, file_name) 107 | zip_file_path = os.path.join(_temp_dir, file_name+'.zip') 108 | if os.path.isfile(zip_file_path): 109 | pass 110 | elif os.path.isdir(file_dir): 111 | message.reply_text(l10n('zip_packing', locale)) 112 | # wait 10 times, 30 seconds for each 113 | for _ in range(10): 114 | if not os.path.isfile(zip_file_path): 115 | time.sleep(30) 116 | else: 117 | break 118 | else: 119 | message.reply_text(l10n('zip_timeout', locale)) 120 | return -1 121 | else: 122 | message.reply_text(l10n('zip_preparing', locale)) 123 | zip_file_path = callback(*callback_args) 124 | 125 | with open(zip_file_path, 'rb') as f: 126 | filename = pack_name or file_name 127 | chat.send_action('upload_document') 128 | chat.send_document(f, filename=filename+'.zip', 129 | reply_to_message_id=message.message_id) 130 | 131 | # count usage 132 | set_usage(context, file_path=zip_file_path) 133 | 134 | except Exception as e: 135 | message.reply_text(l10n('exec_error', locale)) 136 | self.logger.error('Exception msg: %s', str(e), exc_info=True) 137 | 138 | @run_async 139 | def download_sticker_set_async(self, sticker_set_name, update=None, context=None): 140 | if not update and not context: 141 | return -1 142 | 143 | # parse update and context 144 | callback = download_sticker_set 145 | args = (sticker_set_name,) 146 | self.base_send_zip(sticker_set_name, callback, args, update=update, context=context) 147 | 148 | @run_async 149 | def download_gif_pack_async(self, file_id, update=None, context=None): 150 | if not update or not context: 151 | return -1 152 | 153 | # language 154 | locale = update.effective_user.language_code 155 | message = update.effective_message 156 | 157 | try: 158 | # parse update and context 159 | bot = context.bot 160 | gif = bot.get_file(file_id) 161 | gif_pack_name = gif.file_path.split('/')[-1].split('.')[0] 162 | gif_unique_id = random_string() # gif.file_unique_id 163 | 164 | callback = download_gif_pack 165 | args = (gif, gif_pack_name) 166 | self.base_send_zip(gif_pack_name, callback, args, 167 | update=update, context=context, pack_name=gif_unique_id) 168 | 169 | except Exception as e: 170 | message.reply_text(l10n('exec_error', locale)) 171 | self.logger.error('Exception msg: %s', str(e), exc_info=True) 172 | 173 | @run_async 174 | def download_sticker_animated_async(self, file_id, update=None, context=None): 175 | if not update or not context: 176 | return -1 177 | 178 | # language 179 | locale = update.effective_user.language_code 180 | message = update.effective_message 181 | 182 | try: 183 | # parse update and context 184 | bot = context.bot 185 | sticker = bot.get_file(file_id) 186 | sticker_pack_name = sticker.file_path.split('/')[-1].split('.')[0] 187 | sticker_unique_id = random_string() # sticker.file_unique_id 188 | 189 | callback = download_sticker_animated_pack 190 | args = (sticker, sticker_pack_name) 191 | self.base_send_zip(sticker_pack_name, callback, args, 192 | update=update, context=context, pack_name=sticker_unique_id) 193 | 194 | except Exception as e: 195 | message.reply_text(l10n('exec_error', locale)) 196 | self.logger.error('Exception msg: %s', str(e), exc_info=True) 197 | 198 | def download_sticker_async(self, file_id, update=None, context=None): 199 | if not update or not context: 200 | return -1 201 | 202 | # language 203 | locale = update.effective_user.language_code 204 | 205 | # parse update and context 206 | bot = context.bot 207 | sticker = bot.get_file(file_id) 208 | sticker_name = sticker.file_path.split('/')[-1].split('.')[0] 209 | sticker_unique_id = random_string() # sticker.file_unique_id 210 | 211 | chat = update.effective_chat 212 | message = update.effective_message 213 | 214 | # send action 215 | chat.send_action('upload_document') 216 | 217 | # download sticker 218 | file_dir = os.path.join(_temp_dir, sticker_name) 219 | file_path = os.path.join(_temp_dir, sticker_name, sticker_name+'.png') 220 | if os.path.isfile(file_path): 221 | pass 222 | elif os.path.isdir(file_dir): 223 | # wait 10 times, 10 seconds for each 224 | for _ in range(10): 225 | if not os.path.isfile(file_path): 226 | time.sleep(10) 227 | else: 228 | break 229 | else: 230 | message.reply_text(l10n('zip_timeout', locale)) 231 | return -1 232 | else: 233 | # make dir `sticker_name` 234 | os.path.isdir(file_dir) or os.makedirs(file_dir) 235 | _, file_path = download_sticker(sticker, save_dir=file_dir) 236 | 237 | # send document 238 | sticker_set_name = message.sticker.set_name 239 | callback_data = ''.join([STICKER_SET, ':', sticker_set_name]) 240 | keyboard = InlineKeyboardMarkup( 241 | [[ 242 | InlineKeyboardButton(text=l10n('kb_sticker_set', locale), 243 | callback_data=callback_data), 244 | ]] 245 | ) 246 | with open(file_path, 'rb') as f: 247 | chat.send_document(f, reply_markup=keyboard, 248 | filename=sticker_unique_id+'.png', 249 | reply_to_message_id=message.message_id) 250 | 251 | # count usage 252 | set_usage(context, file_path=file_path) 253 | 254 | 255 | ''' command functions ''' 256 | 257 | def cmd_start(self, update, context): 258 | '''''' 259 | # language 260 | locale = update.effective_user.language_code 261 | 262 | user = update.effective_user 263 | user_name = '@' + str(user.username) 264 | first_name = user.first_name 265 | last_name = user.last_name 266 | if first_name and last_name: 267 | user_name = first_name + ' ' + last_name 268 | elif first_name: 269 | user_name = first_name 270 | elif last_name: 271 | user_name = last_name 272 | 273 | update.message.reply_text(l10n('start', locale) % {'user': user_name}) 274 | 275 | def cmd_help(self, update, context): 276 | '''''' 277 | # language 278 | locale = update.effective_user.language_code 279 | update.message.reply_markdown(l10n('help', locale)) 280 | 281 | @check_usage_limit 282 | def cmd_sticker(self, update, context): 283 | '''''' 284 | # language 285 | locale = update.effective_user.language_code 286 | 287 | file_id = update.effective_message.sticker.file_id 288 | if update.effective_message.sticker.is_animated: 289 | update.effective_message.reply_text(l10n('unsupport', locale)) 290 | # print(update.effective_message.sticker) 291 | # self.download_sticker_animated_async(file_id, update=update, context=context) 292 | else: 293 | self.download_sticker_async(file_id, update=update, context=context) 294 | 295 | @check_usage_limit 296 | def cmd_sticker_set(self, update, context): 297 | '''''' 298 | sticker_set_name = context.match.group('sticker_set') 299 | self.download_sticker_set_async(sticker_set_name, update=update, context=context) 300 | 301 | @check_usage_limit 302 | def callback_sticker_set(self, update, context): 303 | '''''' 304 | # answer 305 | update.callback_query.answer() 306 | 307 | # remove keyboard 308 | update.callback_query.edit_message_reply_markup(None) 309 | 310 | # download or send 311 | callback_data = update.callback_query.data 312 | sticker_set_name = callback_data.split(':')[-1] 313 | self.download_sticker_set_async(sticker_set_name, update=update, context=context) 314 | 315 | @check_usage_limit 316 | def cmd_gif(self, update, context): 317 | '''''' 318 | # language 319 | locale = update.effective_user.language_code 320 | 321 | file_id = update.effective_message.document.file_id 322 | file_size = update.effective_message.document.file_size or 1.0e10 323 | if file_size > self.gif_file_size_max: 324 | update.effective_message.reply_text(l10n('file_size_exceed', locale)) 325 | else: 326 | self.download_gif_pack_async(file_id, update=update, context=context) 327 | 328 | def error_handler(self, update, context): 329 | '''''' 330 | self.logger.error('Exception msg: %s\nUpdate: %s', context.error, update) 331 | 332 | def execute(self): 333 | '''''' 334 | # persistence 335 | pp = PicklePersistence(self.persistence_file_path, single_file=False) 336 | 337 | updater = Updater(_telegrambot_token, use_context=True, persistence=pp) 338 | dp = updater.dispatcher 339 | 340 | dp.add_handler(CommandHandler('start', self.cmd_start)) 341 | dp.add_handler(CommandHandler('help', self.cmd_help)) 342 | # sticker 343 | dp.add_handler(MessageHandler(Filters.sticker, self.cmd_sticker)) 344 | # sticker set 345 | dp.add_handler(CallbackQueryHandler(self.callback_sticker_set, pattern='^%s' % (STICKER_SET))) 346 | dp.add_handler(MessageHandler(Filters.regex(r'https://t.me/addstickers/(?P[\s\S]+)'), 347 | self.cmd_sticker_set)) 348 | # gif 349 | dp.add_handler(MessageHandler(Filters.document.gif, self.cmd_gif)) 350 | # error handler 351 | dp.add_error_handler(self.error_handler) 352 | 353 | if not self.cert_path or not self.key_path or not self.webhook_url: 354 | updater.start_polling() 355 | else: 356 | updater.start_webhook(listen='0.0.0.0', port=8443, url_path='gifbot', 357 | cert=self.cert_path, key=self.key_path, 358 | webhook_url=self.webhook_url, 359 | bootstrap_retries=0, 360 | clean=True) 361 | 362 | # Run the bot until you press Ctrl-C or the process receives SIGINT, 363 | # SIGTERM or SIGABRT. This should be used most of the time, since 364 | # start_polling() is non-blocking and will stop the bot gracefully. 365 | updater.idle() 366 | 367 | 368 | if __name__ == '__main__': 369 | BotExecutor().execute() 370 | -------------------------------------------------------------------------------- /telegram_bot/func_helper.py: -------------------------------------------------------------------------------- 1 | '''''' 2 | 3 | import os 4 | import zipfile 5 | from random import shuffle 6 | 7 | 8 | def random_string(length=8): 9 | '''''' 10 | s = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' 11 | s_length = 62 12 | while length > s_length: 13 | s += s 14 | s_length += 62 15 | s_list = list(s) 16 | shuffle(s_list) 17 | return ''.join(s_list[:length]) 18 | 19 | def zip_dir(in_file_dir, out_file_path): 20 | '''''' 21 | f = zipfile.ZipFile(out_file_path, 'w', zipfile.ZIP_DEFLATED) 22 | for dirpath, dirnames, filenames in os.walk(in_file_dir): 23 | fpath = dirpath.replace(in_file_dir, '') 24 | fpath = fpath and fpath + os.sep or '' 25 | for filename in filenames: 26 | f.write(os.path.join(dirpath, filename), fpath+filename) 27 | f.close() 28 | -------------------------------------------------------------------------------- /telegram_bot/gif_downloader.py: -------------------------------------------------------------------------------- 1 | '''''' 2 | 3 | import os 4 | from telegram import Bot 5 | 6 | from global_config.protected_config import _telegrambot_token 7 | from global_config.environment_config import _base_dir, _temp_dir 8 | from telegram_bot.func_helper import random_string, zip_dir 9 | 10 | 11 | class GifDownloader(): 12 | def __init__(self): 13 | '''''' 14 | self.bot = Bot(_telegrambot_token) 15 | 16 | @staticmethod 17 | def mp42gif(in_file_path, out_file_path): 18 | '''''' 19 | palette_path = out_file_path.replace('.gif', '_palette.png') 20 | command = 'ffmpeg -y -i %(mp4)s -vf fps=10,scale=-1:-1:flags=lanczos,palettegen %(palette)s' 21 | command = command % {'mp4': in_file_path, 'palette': palette_path} 22 | status = os.system(command + ' > /dev/null 2>&1') 23 | if status != 0: 24 | os.path.isfile(palette_path) and os.remove(palette_path) 25 | raise Exception('ffmpeg error: execute gif => _palette.png') 26 | 27 | command = 'ffmpeg -y -i %(mp4)s -i %(palette)s -filter_complex "fps=10,scale=-1:-1:flags=lanczos[x];[x][1:v]paletteuse" %(gif)s' 28 | command = command % {'mp4': in_file_path, 'palette': palette_path, 'gif': out_file_path} 29 | status = os.system(command + ' > /dev/null 2>&1') 30 | os.path.isfile(palette_path) and os.remove(palette_path) 31 | if status != 0: 32 | raise Exception('ffmpeg error: execute .gif => .mp4') 33 | else: 34 | return out_file_path 35 | 36 | def download_gif(self, file_id, save_dir=None, random_name=False): 37 | '''''' 38 | gif = file_id if not isinstance(file_id, str) else self.bot.get_file(file_id) 39 | 40 | # use default `temp` path 41 | save_dir = save_dir or _temp_dir 42 | file_name = gif.file_path.split('/')[-1] 43 | if random_name: 44 | file_name = random_string() + '.mp4' 45 | file_path = os.path.join(save_dir, file_name) 46 | out_path = file_path.replace('mp4', 'gif') 47 | 48 | # download and convert 49 | gif.download(custom_path=file_path) 50 | self.mp42gif(file_path, out_path) 51 | 52 | return (file_path, out_path) 53 | 54 | def download_gif_pack(self, file_id, pack_name, out_path=None): 55 | '''''' 56 | # make dir `pack_name` 57 | file_dir = os.path.join(_temp_dir, pack_name) 58 | os.path.isdir(file_dir) or os.makedirs(file_dir) 59 | 60 | # download and convert 61 | self.download_gif(file_id, save_dir=file_dir, random_name=True) 62 | 63 | # zip 64 | out_path = out_path or file_dir + '.zip' 65 | zip_dir(file_dir, out_path) 66 | 67 | return out_path 68 | 69 | 70 | _gif = GifDownloader() 71 | download_gif = _gif.download_gif 72 | download_gif_pack = _gif.download_gif_pack 73 | 74 | 75 | if __name__ == '__main__': 76 | pass 77 | -------------------------------------------------------------------------------- /telegram_bot/sticker_set_downloader.py: -------------------------------------------------------------------------------- 1 | '''''' 2 | 3 | import os 4 | from PIL import Image 5 | from telegram import Bot 6 | 7 | from global_config.protected_config import _telegrambot_token 8 | from global_config.environment_config import _base_dir, _temp_dir 9 | from telegram_bot.func_helper import random_string, zip_dir 10 | 11 | 12 | class StickerSetDownloader(): 13 | def __init__(self): 14 | '''''' 15 | self.num_threads = 4 16 | self.bot = Bot(_telegrambot_token) 17 | 18 | @staticmethod 19 | def webp2png(in_file_path, out_file_path): 20 | '''''' 21 | im = Image.open(in_file_path) 22 | im.save(out_file_path, 'PNG') 23 | return out_file_path 24 | 25 | @staticmethod 26 | def tgs2mp4(in_file_path, out_file_path): 27 | '''''' 28 | json_path = out_file_path.replace('mp4', 'json') 29 | command = 'tgsconvert.py %(tgs)s %(json)s > /dev/null 2>&1' 30 | command = command % {'tgs': in_file_path, 'json': json_path} 31 | status = os.system(command) 32 | if status != 0: 33 | os.path.isfile(json_path) and os.remove(json_path) 34 | raise Exception('tgsconvert.py error: execute .tgs => .json') 35 | 36 | command = 'puppeteer-lottie -q -i %(json)s -o %(mp4)s > /dev/null 2>&1' 37 | command = command % {'json': json_path, 'mp4': out_file_path} 38 | status = os.system(command) 39 | os.path.isfile(json_path) and os.remove(json_path) 40 | if status != 0: 41 | raise Exception('puppeteer-lottie error: execute .json => .mp4') 42 | else: 43 | return out_file_path 44 | 45 | @staticmethod 46 | def mp42gif(in_file_path, out_file_path): 47 | '''''' 48 | command = 'ffmpeg -y -i %(mp4)s -filter_complex "fps=30" %(gif)s' 49 | command = command % {'mp4': in_file_path, 'gif': out_file_path} 50 | status = os.system(command + ' > /dev/null 2>&1') 51 | if status != 0: 52 | raise Exception('ffmpeg error: execute .mp4 => .gif') 53 | else: 54 | return out_file_path 55 | 56 | def download_sticker(self, file_id, save_dir=None, random_name=False): 57 | '''''' 58 | sticker = file_id if not isinstance(file_id, str) else self.bot.get_file(file_id) 59 | 60 | # use default `temp` path 61 | save_dir = save_dir or _temp_dir 62 | file_name = sticker.file_path.split('/')[-1] 63 | if random_name: 64 | file_name = random_string() + '.webp' 65 | file_path = os.path.join(save_dir, file_name) 66 | out_path = file_path.replace('webp', 'png') 67 | 68 | # download and convert 69 | sticker.download(custom_path=file_path) 70 | self.webp2png(file_path, out_path) 71 | 72 | return (file_path, out_path) 73 | 74 | def download_sticker_set(self, sticker_set_name, out_path=None): 75 | '''''' 76 | sticker_set = self.bot.get_sticker_set(sticker_set_name) 77 | stickers = sticker_set.stickers 78 | 79 | # make dir `sticker_set_name` 80 | file_dir = os.path.join(_temp_dir, sticker_set_name) 81 | os.path.isdir(file_dir) or os.makedirs(file_dir) 82 | 83 | # download and convert 84 | for sticker in stickers: 85 | file_id = sticker.file_id 86 | webp_path, _ = self.download_sticker(file_id, save_dir=file_dir, random_name=True) 87 | os.path.isfile(webp_path) and os.remove(webp_path) 88 | 89 | # zip 90 | out_path = out_path or file_dir + '.zip' 91 | zip_dir(file_dir, out_path) 92 | return out_path 93 | 94 | def download_sticker_animated(self, file_id, save_dir=None, random_name=False): 95 | '''''' 96 | sticker = file_id if not isinstance(file_id, str) else self.bot.get_file(file_id) 97 | 98 | # use default `temp` path 99 | save_dir = save_dir or _temp_dir 100 | file_name = sticker.file_path.split('/')[-1] 101 | if random_name: 102 | file_name = random_string() + '.tgs' 103 | file_path = os.path.join(save_dir, file_name) 104 | out_path_mp4 = file_path.replace('tgs', 'mp4') 105 | out_path_gif = file_path.replace('tgs', 'gif') 106 | 107 | # download and convert 108 | sticker.download(custom_path=file_path) 109 | self.tgs2mp4(file_path, out_path_mp4) 110 | self.mp42gif(out_path_mp4, out_path_gif) 111 | 112 | return (file_path, out_path_mp4, out_path_gif) 113 | 114 | def download_sticker_animated_pack(self, file_id, pack_name, out_path=None): 115 | '''''' 116 | # make dir `pack_name` 117 | file_dir = os.path.join(_temp_dir, pack_name) 118 | os.path.isdir(file_dir) or os.makedirs(file_dir) 119 | 120 | # download and convert 121 | self.download_sticker_animated(file_id, save_dir=file_dir, random_name=True) 122 | 123 | # zip 124 | out_path = out_path or file_dir + '.zip' 125 | zip_dir(file_dir, out_path) 126 | 127 | return out_path 128 | 129 | 130 | _sticker = StickerSetDownloader() 131 | download_sticker = _sticker.download_sticker 132 | download_sticker_set = _sticker.download_sticker_set 133 | download_sticker_animated_pack = _sticker.download_sticker_animated_pack 134 | 135 | 136 | if __name__ == '__main__': 137 | pass 138 | --------------------------------------------------------------------------------