├── .github └── workflows │ ├── Docker-Build-Guide.md │ ├── Docker-Code │ ├── Fly.yml │ └── docker-publish.yml ├── Commands.md ├── Dockerfile ├── Img ├── 1.png ├── 10.png ├── 11.png ├── 12.png ├── 13.png ├── 14.png ├── 15.png ├── 16.png ├── 17.png ├── 18.png ├── 19.png ├── 2.png ├── 20.png ├── 21.png ├── 22.png ├── 23.png ├── 24.png ├── 25.png ├── 26.png ├── 27.png ├── 28.png ├── 29.png ├── 3.png ├── 30.png ├── 31.png ├── 32.png ├── 33.png ├── 34.png ├── 35.png ├── 36.png ├── 37.png ├── 38.png ├── 39.png ├── 4.png ├── 40.png ├── 41.png ├── 42.png ├── 43.png ├── 44.png ├── 45.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png ├── 9.png ├── Deploy-Button-Heroku.png └── deleteme.txt ├── LICENCE ├── Procfile ├── README.md ├── Termux-Guide.md ├── _config.yml ├── captain-definition ├── docker-compose.yml ├── fly.toml ├── index.html ├── requirements.txt ├── run.sh ├── sample-config.ini └── telegram_gcloner ├── config.ini ├── handlers ├── add_group.py ├── ban.py ├── cancel.py ├── choose_folder.py ├── contact.py ├── get_help.py ├── get_id.py ├── process_drive_links.py ├── process_message.py ├── sa.py ├── start.py ├── stop_task.py └── vip.py ├── telegram_gcloner.py └── utils ├── callback.py ├── config_loader.py ├── fire_save_files.py ├── google_drive.py ├── helper.py ├── process.py └── restricted.py /.github/workflows/Docker-Build-Guide.md: -------------------------------------------------------------------------------- 1 |
As per your requirements, or to run CloneBot V2 easily on your OS and depending upon its architecture you can make your own Docker Image for CloneBot V2, the process of building docker image is automated and easy, you just need to edit Dockerfile
available in root directory of main
branch and trigger the Workflow from Actions
Tab and it will start building your Docker Image.
For quickly building the same Docker Image used by CloneBot V2:
5 |.github/workflows
in main
branch.Docker-Code
file and copy its code.main
branch and paste the copied code (by removing previous code) in Dockerfile
.Actions
tab and run the Publish Docker Image
workflow! and it will start building your Docker Image.Packages
to know how to use it.😊⛔NOTE: Use your own Docker Image for deploying on VPS only! Using it for deploying platforms like Heroku will simply cause Account suspension.
15 |You can also customize the behaviour of Docker Image Build tool as per your needs!😉
17 |To set the condition "When Workflow should be triggered?", you can customize following code:
19 | https://github.com/TheCaduceus/CloneBot_V2/blob/dbbd61dc0430a5bc8eda672ef4e123a9ee5c2794/.github/workflows/docker-publish.yml#L3-L12 20 |by default, Workflow will be triggered only if user manually do it from Actions
Tab otherwise if automatic workflow trigger is enabled then it will get triggered automatically when there is new commit (including Pull Request) in main
branch which can be changed.
By setting environment variables you can change the Registry and Name of your Docker Image:
23 | https://github.com/TheCaduceus/CloneBot_V2/blob/dbbd61dc0430a5bc8eda672ef4e123a9ee5c2794/.github/workflows/docker-publish.yml#L14-L18 24 |REGISTRY
: Value can be docker.io
or ghcr.io
. If empty then docker.io
will be used.
IMAGE_NAME
: Value can be anything between ""
or by default it is ${{ github.repository }}
which automatically set Repository name + Branch Name as IMAGE_NAME
.
CloneBot V2 is inspired from MsGsuite's CloneBot, which got out-dated and having too many errors in it. We both created it to keep the legacy of CloneBot alive! The bot who helped thousands for cloning their data.❤️
4 |1. The Powerful Telegram Bot based on Gclone to clone Google Drive's Shared Drive data easily.⚡
5 |2. CloneBot V2 usage Service Accounts to easily clone TBs of data without hitting 750GB Upload/Clone limit of Google Drive.♻️
6 |3. It is most lightweight and performs only server-sided cloning to have very less load on system and don't use your own bandwidth.🗃️
7 |4. Just provide the sharing link of a particular Shared Drive/folder or file and set multiple destination folders to clone data.🔗
8 | 9 |Easily navigate through out the guide and learn about Powerful CloneBot V2 and terms related to it.
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |➥🐳Build or Deploy using Docker
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |1.Gclone upgraded to v1.59.1 (latest)!😉
41 |2.UI Changes!🌟
42 |3.CloneBot V2 is now comfortable with Python 3.10.6
🐍.
4.Resolved $PORT listening Errors in Okteto and other platforms.⚙️
44 |5.Old Docker Image ghcr.io/thecaduceus/clonebot_v2:main
is now no more supported and deprecated!🧹
6.Lots of other fixes, changes and improvements which can be checked in Changelog
.
Full Changelog: V2.1.4...v2.2.9
1.You may need account for Fly.io/Clever-Cloud/Okteto/Scalingo while deploying CloneBot V2 on respected platforms.
50 |2.Service Accounts are mandatory to use CloneBot V2, because it uses Service Accounts to prevent hitting 750GB Upload/Clone limit of Google Drive while cloning large amount of data.
51 |3.VPS or your local machine (PC or Laptop or Mobile) should have Python 3
and Git
installed in order to run CloneBot V2.
4.CloneBot V2 don't use your bandwidth or Internet connection while cloning data but it can if hosted on your local machine or VPS for calling required Telegram APIs to update the progress or to generate required response.
53 |5.This Project comes with GNU License, please consider reading it before using this.
54 |6.Name of zip file should be only accounts.zip
and it should only contain .json
files not folders!
7.Don't blame contributors of CloneBot V2 in case your account got suspended while using it by deploying on free services provided below! (We already provided you the details that you should follow to prevent it if you are new to this platforms) on Clever-Cloud add Credit card before deploying your bot on it, only report error which is releated with code of CloneBot V2! we don't accept problems regarding any platform on which you are going to deploy this.
56 |8.Don't get confused! If you use pip
to install requirements.txt
then only use py
or python
for executing commands or in the same way if you use pip3
then only use python3
.
9.Aim of CloneBot V2 is not to violate any platform's TOS and hence we removed deployment support of platforms like Heroku, don't create an issue or PR for adding support of Heroku or platforms which don't allow it or if CloneBot V2 violate their TOS.
58 |10.PRs for just changing the status message or similar is not accepted! that does not mean that PRs including Typo Errors will be rejected.
59 | 60 |CloneBot V2 is very straight forward and easy to use bot. If you deployed your CloneBot V2 then consider adding commands in it through @BotFather to make it easy for other users to know bot commands, here is the commands list to be set in @BotFather:
62 |accounts.zip
then send it to bot and write /sa
in caption or send /sa
as reply to accounts.zip
file. Don't have Service Accounts? Learn here how to create/folders
to your CloneBot V2 and then bot will show Shared Drives name in which you added your Service Accounts's Google Group, select Shared Drive or directory available in it as destination. Not added Service Accounts in Google Group? Learn here how to do./ban
and /unban
command is to unauthorize or authorize user again and /id
command is to get your Telegram User ID.⛔NOTE: Each allowed user have to upload their own accounts.zip
to use CloneBot V2.
CloneBot V2 comes with the ability to clone data between My Drive to Shared Drive or Shared Drive to Shared Drive, but in both case Shared Drive is common & required! So lets see how we can create our own Shared Drive for free to use them with CloneBot V2.
72 |Shared Drive Name
: Enter Name which you want to give to your Shared Drive. It can be anything but avoid using Emojis to prevent UTF-8 Errors
Gmail ID
: Enter your Google Account's Email ID for which you want to create Shared Drive.
Domain Selection
: Using drop-down list, select a working domain through which you want to create Shared Drive, or if you are not sure then keep it as Random
.
CREATE
./start
, then click CREATE TD
.⛔NOTE: Shared Drive is a temporary storage! use it carefully and keep backup of your data always with you.
89 |Generally, I seen people, blindly running and ignoring options provided by Setup for installing Python and PIP which is most important thing to make Service Accounts or to run CloneBot V2. People like it are just there to flood out support chat and abuse moderators too! Hence I made this Section to tackle this special disease. Lets name this disease Setup-Blindness😂
91 |Customize Installation
:Next
.Install
and you done it!'python3' is not recognized as an internal or external command,
99 | operable program or batch file.
'python' is not recognized as an internal or external command,
101 | operable program or batch file.
'py' is not recognized as an internal or external command,
103 | operable program or batch file.
'pip3' is not recognized as an internal or external command,
105 | operable program or batch file.
'pip' is not recognized as an internal or external command,
107 | operable program or batch file.
Deployment of CloneBot V2 is as simple as its usage! Their are many methods listed below to deploy CloneBot easily, but before you deploy it, you need some values listed below and how to get it:
111 |
112 | path_to_gclone
- Path to gclone file, by default it is gclone
or change it if you using different one.
113 | telegram_token
- Get your bot's Telegram API Token from BotFather.
114 | user_ids
- Telegram User IDs of users who can use your CloneBot_V2. Separate them using ,
and first User ID is Admin.
115 | group_ids
- Telegram Group IDs of Groups in which CloneBot can be used otherwise keep it -1
. Separate them using ,
116 | gclone_para_override
- Keep it blank if you don't know what it is.
117 |
⛔NOTE: Everything in config.ini
should be Int
.
CONFIG_FILE_URL
is URL to config.ini
file which contains values of variables discussed above, lets see how to get your CONFIG_FILE_URL
easily:
Code
as well as Raw
option then paste the variables discussed above!View Raw
button and copy the URL from address bar which you will get after pressing it.config.ini
file and send that file to the bot and get permanent working link.CONFIG_FILE_URL
is now ready to be used.config.ini
and now fill below values as discussed above!Create Secret Gist
then click Raw
, it will open a New Tab in your Browser. Just copy the URL of that New TabCommit_ID
from the URL:Before:
141 | https://gist.githubusercontent.com/UserName/0ee24eXXXXXXXXXXXXXXX6b/raw/Commit_ID
/config.ini
142 | After:
143 | https://gist.githubusercontent.com/UserName/0ee24eXXXXXXXXXXXXXXX6b/raw/config.ini
144 |
CloneBot V2 can be deployed almost everywhere using Docker, either you can create your own Docker Image using Build Tool provided in the Workflow including Docker-Code
. While CloneBot V2 also have ready to use Docker image for systems based on AMD 64
.
->docker pull ghcr.io/thecaduceus/clonebot-v2:main
->FROM ghcr.io/thecaduceus/clonebot-v2:main
ghcr.io/thecaduceus/clonebot_v2:main
is now no more supported and deprecated!⛔NOTE:
155 |1.Docker Image only accepts CONFIG_FILE_URL
2.Use your own Docker Image for deploying on VPS only! Using it for deploying on platforms like Heroku, Okteto or Scalingo will simply cause Account suspension.
157 |Fly.io is platform and best alternative of Heroku (Salesforce) becuase here you can deploy your apps by just adding Credit Card (without being charged) or anyother payment methods, unlike Heroku, they offers you 2,340 running hours per month while Heroku only provides 550 running hours (dyno hours) to run your app! that means you don't have to worry about suddenly getting your app stopped like in the case of Heroku. Fly.io also not restarts your app each 24 hours which enables you to clone bigger data easily.
159 |MacOS / Linux:
163 |curl -L https://fly.io/install.sh | sh
Using Brew:
165 |brew install flyctl
Windows Powershell:
167 |iwr https://fly.io/install.ps1 -useb | iex
Termux: (Refer #54)
169 |pkg install flyctl
git clone https://github.com/TheCaduceus/CloneBot_V2
174 | cd CloneBot_V2
- To change directory.
175 | fly auth login
- To login on Fly.io.
176 | fly launch
- To configure basic things, like app name and data center as well as creating fly.toml
.
177 |
1.For app name keep the field empty (Hit Enter
), and for choosing data center! use arrow keys to select one. For attaching Postgres Database enter
180 | N
including for Deploy Now.
2.Once you run the above command! it will automatically create fly.toml
file, open the fly.toml
file with any text editor and under [env]
section put your CONFIG_FILE_URL
which you created above!
3.Everything done! now run the final deploy command to deploy your app.
185 |fly deploy
- To deploy your app.
⛔NOTICE: You can use flyctl
instead of fly
.
CloneBot V2 can also be deployed on Fly.io using GitHub Actions, this method is useful if you don't have PC or you can't download flyctl
on Termux due to architecture limitations.
1.Set following secret in GitHub Secrets:
190 |FLY_API_TOKEN
: Get your Fly API Token from here.
APP_NAME
: Fly App name of your choice
CONFIG_FILE_URL
: CONFIG_FILE_URL
created above
2.Go to Actions
Tab and run Deploy to Fly
workflow.
194 |
Clever Cloud is a Europe-based PaaS (Platform as a Service) company. They help developers deploy and run their apps with bulletproof infrastructure, automatic scaling as well as fair pricing. In my opinion! it is best choice to deploy CloneBot V2 on Clever Cloud because pricing is excellent & fair as well as you can run CloneBot V2 for days to clone large amount of data.
196 |⛔NOTICE: Before deploying/running CloneBot V2 on Clever Cloud! Don't forget to add payment method like credit card in your account to verify your account otherwise deploying and using CloneBot V2 on Clever Cloud will cause suspension of your app/account.
197 |Create
and then select an application
from the list.Docker
.😘Next
on "How many number of instances?" page and keep the number of instance only 1. Additionally, you can keep instance type to Nano
which is most cheap because CloneBot V2 is designed to run on very low end systems.Paris France
for lower ping (tested!😉).I DON'T NEED ANY ADD-ONS
because... you already know it!🌟 still why? it is designed for low end systems.CONFIG_FILE_URL
as variable name and the CONFIG_FILE_URL
which you just made here! and Clever Cloud will start deploying your instance.Domain Names
tab and for logs you can check Logs
tab.Okteto is Kubernetes development platforms and used by many users and it is ideal for lightweight apps and it is perfect for CloneBot V2, Okteto is worst than Heroku, your bot will sleep after 24 hours and will not get back to online until you ping the provided ENDPOINT.
218 |main
and add following value carefully:
224 | CONFIG_FILE_URL
- Enter CONFIG_FILE_URL
, which you just made here.
225 |
⛔NOTE: Don't forget to setup Cron-Job for Okteto otherwise your deployed bot will go into sleep and you have to active it from Okteto Dashboard, while Cron-Job doing it on your behalf.
235 | 236 |Running CloneBot V2 on your PC or VPS is very simple and takes very less efforts! It have very less load on your System and don't use your bandwidth or Internet connection for cloning Google Drive data but only for calling Telegram APIs to update the progress or to generate required response.
238 |
240 | ->Python 3 or above with pip
241 | ->Git
242 |
245 | ->git clone https://github.com/TheCaduceus/CloneBot_V2
246 | ->Or Download from Here
247 |
250 | ->cd CloneBot_V2
251 | ->pip install -r requirements.txt
252 |
255 | ->Go to Gclone Library and download Gclone file as per your Operating System and place it in "telegram_gcloner" folder.
256 | ->Website provides direct download link, so you can also use Command-line to download Gclone.
257 | Linux:
258 | ->curl download_link_here >> telegram_gcloner/gclone
259 | Windows:
260 | ->curl download_link_here >> telegram_gcloner/gclone.exe
261 |
Config.ini
file
264 | ->Open Config.ini
file in any text editor and enter the values of variables as written here
265 |
Or you can download your Config.ini
file from external source using CONFIG_FILE_URL by using Command-line:
266 | ->curl CONFIG_FILE_URL >> telegram_gcloner/config.ini
267 |
270 | ->cd CloneBot_V2
271 | ->python telegram_gcloner/telegram_gcloner.py
272 |
275 | ->Press CTRL
+ C
keys
276 |
Termux is a best app for running and using Command-line tools on Mobile, CloneBot can also be deploy on your Mobile using Termux itself, don't worry because CloneBot V2 is very lightweight and designed to be deployed even on low-end systems and thus it will not cause heavy load on your Mobile.
280 |CloneBot V2 is also deployable to Scalingo cloud, Just deploy Scalingo
Branch.
Switch to Scalingo Branch for guide.
286 | 287 |Service Accounts are just like normal Google Account and thus have same Upload or Download limits as Google Account which is 750GB Upload and 10TB Download. They are used to act on behalf of a Google Account and hence we can use them to prevent hitting Google Drive limits by creating them in a bulk amount. After creating Service Accounts, we have to add them in Google Group so that we can directly add Google Group's Email ID in Shared Drive at place of adding each Service Accounts manually.
289 |credentials.json
in the folder you just created.⛔NOTE: Download json file as credentials.json
only!
gen_sa_accounts.py
rename_script.py
as well as requirements.txt
files to folder in which you downloaded credentials.json
.cd
command like cd FOLDER_PATH
in CMD.
329 | 1. pip install -U -r requirements.txt
- To install requirements.
330 | 2. py gen_sa_accounts.py
- To get login URL.
331 |
⛔NOTE: Login only with Google account which you used to create Project on Google Cloud Console.
334 |
339 | 3. py gen_sa_accounts.py --list-projects
- To get the ID of your created Project.
340 | 4. py gen_sa_accounts.py --enable-services PROJECT_ID
- To Enable Services in given project.
341 | 5. py gen_sa_accounts.py --create-sas PROJECT_ID
- To create Service Accounts.
342 | 6. py gen_sa_accounts.py --download-keys PROJECT_ID
- To download Service Accounts file.
343 | 7. py rename_script.py
- To rename Service Accounts file in 1-100 sequence.
344 |
⛔NOTE: Replace PROJECT_ID
with Project ID which you will get from command 3 and if commands not working then replace py
with python
or python3
.
accounts
folder in it which have your 100 Service Accounts file (json files), now type "Powershell" in address bar of accounts folder or as an alternative you can use cd
commands like cd FOLDER_PATH
in Powershell.MacOS / Linux:
350 |
351 | grep -oPh '"client_email": "\K[^"]+' *.json > emails.txt
352 |
Windows:
354 |
355 | $emails = Get-ChildItem .\**.json |Get-Content -Raw |ConvertFrom-Json |Select -ExpandProperty client_email >>emails.txt
356 |
accounts
folder into emails.txt
file. Move emails.txt
file from accounts folder to prevent confusion or any other problem.emails.txt
file which you got from STEP 19 then copy & paste 10 Email IDs in the field named "Group Managers". In this way! add all 100 Email IDs in your Google Group but only 10 Email IDs at once.XXXXX@googlegroups.com
and add it in your Shared Drives as "Manager".Dr.Caduceus: For making this Project and Guide.
379 |Levi: For Gclone and upgrading it.
380 |
382 | wrenfairbank: For the original python script.
383 | smartass08: To adapt the scrip to heroku.
384 | anymeofu: For making the Direct Heroku deployable Version.
385 | Zero-The-Kamisama: To making MsGsuite discover this amazing bot and the detailed instructions.
386 | zorgof: For the termux script.
387 | Aishik Tokdar: For Adding Guide to Deploy on Railway.app , Qovery , Clever Cloud , Scalingo and some other Code Improvements.Also Added Heroku Workflow Deployment Method.
388 | Katarina: For adding the ability to be deployed to Clever Cloud and Scalingo.
389 | Miss Emily: For adding Support of Okteto Cloud Deployment as well as improving little layout.
390 |
{html.escape(str(e))}
',
61 | parse_mode=ParseMode.HTML,
62 | )
63 |
64 | return
65 |
66 | query = update.callback_query
67 | match = re.search(
68 | f'^{callback_query_prefix},(?P{}
'.format(html.escape(str(e))),
113 | parse_mode=ParseMode.HTML)
114 | return
115 |
116 | if context.args:
117 | current_folder_id = context.args[0]
118 | try:
119 | gd.get_file_name(current_folder_id)
120 | folders = gd.list_folders(current_folder_id)
121 | except Exception as e:
122 | folders = gd.get_drives()
123 | current_folder_id = ''
124 | context.bot.send_message(chat_id=update.effective_user.id,
125 | text='Error:\n{}
'.format(html.escape(str(e))),
126 | parse_mode=ParseMode.HTML)
127 |
128 | callback_query_prefix = 'choose_folder'
129 | query = update.callback_query
130 | page = None
131 | message_id = -1
132 | if not query:
133 | rsp = update.message.reply_text('⚙️ Getting Directory ⚙️')
134 | rsp.done.wait(timeout=60)
135 | message_id = rsp.result().message_id
136 | if not folders:
137 | folders = gd.get_drives()
138 | context.user_data[udkey_folders_cache] = copy.deepcopy(folders)
139 |
140 | if query:
141 | logger.debug('{}: {}'.format(update.effective_user.id, query.data))
142 | if query.message.chat_id < 0 and \
143 | (not query.message.reply_to_message or
144 | query.from_user.id != query.message.reply_to_message.from_user.id):
145 | alert_users(context, update.effective_user, 'invalid caller', query.data)
146 | query.answer(text='Yo-he!', show_alert=True)
147 | return
148 | message_id = query.message.message_id
149 | match = re.search(r'^(?P{}
'.format(html.escape(str(e))),
164 | parse_mode=ParseMode.HTML)
165 | context.user_data[udkey_folders_cache] = copy.deepcopy(folders)
166 | if not folders:
167 | folders = {'#': '(No subfolders)'}
168 | match_folder_id_replace = match.group('replace')
169 | if match_folder_id_replace:
170 | context.user_data[udkey_fav_folders_replace] = match_folder_id
171 | if match.group('page'):
172 | page = int(match.group('page'))
173 | if not folders and match.group('page'):
174 | folders = context.user_data.get(udkey_folders_cache, None)
175 | if not folders:
176 | folders = gd.get_drives()
177 | context.user_data[udkey_folders_cache] = copy.deepcopy(folders)
178 | if not folders:
179 | folders = {'#': 'I could not find any Shared Drives associated with your Service Accounts. \n If you don`t have no shared drives, go to @MsGsuite to get one for yourself.'}
180 | else:
181 | alert_users(context, update.effective_user, 'invalid query data', query.data)
182 | query.answer(text='Yo-he!', show_alert=True)
183 | return
184 |
185 | if not page:
186 | page = 1
187 |
188 | folders_len = len(folders)
189 | page_data = []
190 | for item in folders:
191 | page_data.append({'text': folders[item], 'data': item})
192 |
193 | page_data_chosen = list(context.user_data.get(udkey_folders, {}))
194 | inline_keyboard_drive_ids = get_inline_keyboard_pagination_data(
195 | callback_query_prefix,
196 | page_data,
197 | page_data_chosen=page_data_chosen,
198 | page=page,
199 | max_per_page=10,
200 | )
201 |
202 | if current_folder_id:
203 | current_path = ''
204 | current_path_list = gd.get_file_path_from_id(current_folder_id)
205 | if current_path_list:
206 | current_folder_name = current_path_list[0]['name']
207 | for item in current_path_list:
208 | current_path = '/{}{}'.format(item['name'], current_path)
209 | if len(current_path_list) > 1:
210 | inline_keyboard_drive_ids.insert(
211 | 0, [InlineKeyboardButton('📁 ' + current_path,
212 | callback_data='{},{}'.format(
213 | callback_query_prefix, current_path_list[1]['folder_id']))])
214 | else:
215 | inline_keyboard_drive_ids.insert(
216 | 0, [InlineKeyboardButton('📁' + current_path,
217 | callback_data=callback_query_prefix)])
218 | inline_keyboard_drive_ids.append(
219 | [InlineKeyboardButton('✔️ Select this folder({})'.format(current_folder_name),
220 | callback_data='chosen_folder,{}'.format(current_folder_id))])
221 | inline_keyboard_drive_ids.append([InlineKeyboardButton('🔙 Go back',
222 | callback_data='choose_folder' if current_folder_id else '#'),
223 | InlineKeyboardButton('Cancel', callback_data='cancel')])
224 | context.bot.edit_message_text(chat_id=update.effective_chat.id,
225 | message_id=message_id,
226 | text='🔶 Select the directory you wish to add to Favourite Folders and also want to use for cloning 🔶 \n 🔶🔶 There are {} subdirectories found 🔶🔶'.format(
227 | folders_len),
228 | reply_markup=InlineKeyboardMarkup(inline_keyboard_drive_ids))
229 |
230 |
231 | @restricted
232 | def set_folders(update, context):
233 | if update.effective_user.id in config.USER_IDS\
234 | or (context.bot_data.get('vip', None) and update.effective_user.id in context.bot_data['vip']):
235 | max_folders = default_max_folders_vip
236 | else:
237 | max_folders = default_max_folders
238 |
239 | callback_query_prefix = 'choose_folder'
240 | query = update.callback_query
241 | page = 1
242 | if not query:
243 | rsp = update.message.reply_text('⚙️ Getting Favourite Shared Drives ⚙️')
244 | rsp.done.wait(timeout=60)
245 | message_id = rsp.result().message_id
246 | else:
247 | if query.message.chat_id < 0 and \
248 | (not query.message.reply_to_message or
249 | query.from_user.id != query.message.reply_to_message.from_user.id):
250 | alert_users(context, update.effective_user, 'invalid caller', query.data)
251 | query.answer(text='Yo-he!', show_alert=True)
252 | return
253 | message_id = query.message.message_id
254 | folder_ids = context.user_data.get(udkey_folders, None)
255 |
256 | if folder_ids:
257 | folder_ids_len = len(folder_ids)
258 | page_data = []
259 | for item in folder_ids:
260 | page_data.append({'text': simplified_path(folder_ids[item]['path']), 'data': '{}'.format(item)})
261 | inline_keyboard_drive_ids = get_inline_keyboard_pagination_data(
262 | callback_query_prefix + '_replace',
263 | page_data,
264 | page=page,
265 | max_per_page=10,
266 | )
267 | else:
268 | inline_keyboard_drive_ids = []
269 | folder_ids_len = 0
270 | if folder_ids_len < max_folders:
271 | inline_keyboard_drive_ids.insert(0, [InlineKeyboardButton('➕ Add Favorite Folder', callback_data=callback_query_prefix)])
272 | inline_keyboard_drive_ids.append([InlineKeyboardButton('✔️ Done', callback_data='cancel')])
273 |
274 | context.bot.edit_message_text(chat_id=update.effective_chat.id,
275 | message_id=message_id,
276 | text='📁 Total No of Destination Folders {}/{} 📁:'.format(
277 | folder_ids_len,
278 | max_folders,
279 | ),
280 | reply_markup=InlineKeyboardMarkup(inline_keyboard_drive_ids))
281 |
--------------------------------------------------------------------------------
/telegram_gcloner/handlers/contact.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import html
4 | import logging
5 |
6 | from telegram import ParseMode
7 | from telegram.ext import Dispatcher, CommandHandler
8 | from telegram.utils.helpers import mention_html
9 |
10 | from utils.callback import callback_delete_message
11 | from utils.config_loader import config
12 | from utils.restricted import restricted_private
13 |
14 | logger = logging.getLogger(__name__)
15 |
16 |
17 | def init(dispatcher: Dispatcher):
18 | """Provide handlers initialization."""
19 | dispatcher.add_handler(CommandHandler('contact', contact, pass_args=True))
20 |
21 |
22 | @restricted_private
23 | def contact(update, context):
24 | if text := update.message.text.strip('/contact'):
25 | context.bot.send_message(
26 | chat_id=config.USER_IDS[0],
27 | text=f'📬 Received message from {mention_html(update.effective_user.id, html.escape(update.effective_user.name))} ({update.effective_user.id}):',
28 | parse_mode=ParseMode.HTML,
29 | )
30 |
31 | context.bot.forward_message(chat_id=config.USER_IDS[0],
32 | from_chat_id=update.message.chat_id,
33 | message_id=update.message.message_id)
34 | logger.info(
35 | f'{update.effective_user.name} ({update.effective_user.id}) left a message: {text}'
36 | )
37 |
38 | rsp = update.message.reply_text('👍 Roger that Master 👍')
39 | else:
40 | rsp = update.message.reply_text('You\'re so shy, don\'t you want to say anything?\n' +
41 | config.AD_STRING.format(context.bot.username),
42 | ParseMode.HTML)
43 | rsp.done.wait(timeout=60)
44 | message_id = rsp.result().message_id
45 | if update.message.chat_id < 0:
46 | context.job_queue.run_once(callback_delete_message, config.TIMER_TO_DELETE_MESSAGE,
47 | context=(update.message.chat_id, message_id))
48 | context.job_queue.run_once(callback_delete_message, config.TIMER_TO_DELETE_MESSAGE,
49 | context=(update.message.chat_id, update.message.message_id))
50 |
--------------------------------------------------------------------------------
/telegram_gcloner/handlers/get_help.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import logging
4 |
5 | from telegram.ext import Dispatcher, CommandHandler
6 |
7 | from utils.config_loader import config
8 | from utils.callback import callback_delete_message
9 | from utils.restricted import restricted
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | def init(dispatcher: Dispatcher):
15 | """Provide handlers initialization."""
16 | dispatcher.add_handler(CommandHandler('help', get_help))
17 |
18 |
19 | @restricted
20 | def get_help(update, context):
21 | message = 'Send a Google Drive link, or forward a message with a Google Drive link to manually transfer.\n' \
22 | 'Configuration with /sa and /folders is required.\n\n' \
23 | '📚 Commands:\n' \
24 | ' │ /start - Start the Bot' \
25 | ' │ /folders - Set favorite folders\n' \
26 | ' │ /sa - Private chat only, upload a ZIP containing SA accounts with this command as the subject.\n' \
27 | ' │ /ban - Ban a Telegram User ID from using the Bot' \
28 | ' │ /unban - Reallow a Telegram User ID from using the Bot that was earlier banned' \
29 | ' │ /id - Get your Telegram User ID' \
30 | ' │ /contact - Get the contacts details of the owner of the Bot' \
31 | ' │ /help - Output this message\n'
32 |
33 | rsp = update.message.reply_text(message)
34 | rsp.done.wait(timeout=60)
35 | message_id = rsp.result().message_id
36 | if update.message.chat_id < 0:
37 | context.job_queue.run_once(callback_delete_message, config.TIMER_TO_DELETE_MESSAGE,
38 | context=(update.message.chat_id, message_id))
39 | context.job_queue.run_once(callback_delete_message, config.TIMER_TO_DELETE_MESSAGE,
40 | context=(update.message.chat_id, update.message.message_id))
--------------------------------------------------------------------------------
/telegram_gcloner/handlers/get_id.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import logging
4 |
5 | from telegram.ext import Dispatcher, CommandHandler
6 |
7 | from utils.callback import callback_delete_message
8 | from utils.config_loader import config
9 | from utils.restricted import restricted
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | def init(dispatcher: Dispatcher):
15 | """Provide handlers initialization."""
16 | dispatcher.add_handler(CommandHandler('id', get_id))
17 |
18 |
19 | @restricted
20 | def get_id(update, context):
21 | logger.info('Telegram User {0} has requested its ID.'.format(update.effective_user.id))
22 | rsp = update.message.reply_text(update.effective_user.id)
23 | rsp.done.wait(timeout=60)
24 | message_id = rsp.result().message_id
25 |
26 | if update.message.chat_id < 0:
27 | context.job_queue.run_once(callback_delete_message, config.TIMER_TO_DELETE_MESSAGE,
28 | context=(update.message.chat_id, message_id))
29 | context.job_queue.run_once(callback_delete_message, config.TIMER_TO_DELETE_MESSAGE,
30 | context=(update.message.chat_id, update.message.message_id))
31 |
--------------------------------------------------------------------------------
/telegram_gcloner/handlers/process_drive_links.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import html
4 | import logging
5 | import re
6 |
7 | from telegram import ParseMode, InlineKeyboardButton, InlineKeyboardMarkup
8 | from telegram.ext import Dispatcher, CallbackQueryHandler
9 |
10 | from utils.fire_save_files import MySaveFileThread, thread_pool
11 | from utils.google_drive import GoogleDrive
12 | from utils.helper import parse_folder_id_from_url, alert_users, get_inline_keyboard_pagination_data, simplified_path
13 |
14 | logger = logging.getLogger(__name__)
15 |
16 | udkey_folders = 'folder_ids'
17 |
18 |
19 | def init(dispatcher: Dispatcher):
20 | """Provide handlers initialization."""
21 | dispatcher.add_handler(CallbackQueryHandler(save_to_folder_page,
22 | pattern=r'^save_to_folder_page#\d+$'))
23 | dispatcher.add_handler(CallbackQueryHandler(save_to_folder,
24 | pattern=r'^save_to_folder(:?_page#\d+)?\,\s*[\dA-Za-z\-_]+$'))
25 |
26 |
27 | def parse_entity_for_drive_id(message):
28 | if message.photo:
29 | entities = message.parse_caption_entities()
30 | else:
31 | entities = message.parse_entities()
32 |
33 | folder_ids = {}
34 | k = 0
35 |
36 | for entity in entities:
37 | if entity.type == 'text_link':
38 | url = entity.url
39 | name = entities[entity]
40 | elif entity.type == 'url':
41 | url = entities[entity]
42 | name = 'file{:03d}'.format(k)
43 | else:
44 | continue
45 |
46 | logger.debug('Found {0}: {1}.'.format(name, url))
47 | folder_id = parse_folder_id_from_url(url)
48 | if not folder_id:
49 | continue
50 | folder_ids[folder_id] = name
51 |
52 | logger.debug('Found {0} with folder_id {1}.'.format(name, folder_id))
53 |
54 | if not folder_ids:
55 | logger.debug('Cannot find any legit folder id.')
56 | return None
57 | return folder_ids
58 |
59 |
60 | def process_drive_links(update, context):
61 | if not update.message:
62 | return
63 |
64 | folder_ids = parse_entity_for_drive_id(update.message)
65 |
66 | if not folder_ids:
67 | return
68 | message = '📑 The Following Files were Detected : 📑\n'
69 |
70 | try:
71 | gd = GoogleDrive(update.effective_user.id)
72 | except Exception as e:
73 | update.message.reply_text(
74 | f'🔸 Please make sure the SA archive has been uploaded and the collection folder has been configured. 🔸\n{e}'
75 | )
76 |
77 | return
78 |
79 | for item in folder_ids:
80 | try:
81 | folder_name = gd.get_file_name(item)
82 | except Exception as e:
83 | update.message.reply_text(
84 | f'🔸 Please make sure that the SA archive has been uplaoded and yuor SA have rights to read files from the Source Link. 🔸\n{e}'
85 | )
86 |
87 | return
88 | message += f' {html.escape(folder_name)}\n'
89 |
90 | message += '\n📂 Please select the Target Destination 📂'
91 | if fav_folder_ids := context.user_data.get(udkey_folders, None):
92 | page_data = [
93 | {
94 | 'text': simplified_path(fav_folder_ids[item]['path']),
95 | 'data': f'{item}',
96 | }
97 | for item in fav_folder_ids
98 | ]
99 |
100 | callback_query_prefix = 'save_to_folder'
101 | page = 1
102 | inline_keyboard_drive_ids = get_inline_keyboard_pagination_data(
103 | callback_query_prefix,
104 | page_data,
105 | page=page,
106 | max_per_page=10,
107 | )
108 | else:
109 | inline_keyboard_drive_ids = [[InlineKeyboardButton(text='⚠️ Use /folders to add a destination to Favourite Folders List ⚠️', callback_data='#')]]
110 | inline_keyboard = inline_keyboard_drive_ids
111 | update.message.reply_text(message, parse_mode=ParseMode.HTML,
112 | disable_web_page_preview=True, reply_markup=InlineKeyboardMarkup(inline_keyboard))
113 |
114 |
115 | def save_to_folder_page(update, context):
116 | query = update.callback_query
117 | if query.message.chat_id < 0 and \
118 | (not query.message.reply_to_message or
119 | query.from_user.id != query.message.reply_to_message.from_user.id):
120 | alert_users(context, update.effective_user, 'invalid caller', query.data)
121 | query.answer(text='Yo-he!', show_alert=True)
122 | return
123 | match = re.search(r'^save_to_folder_page#(\d+)$', query.data)
124 | if not match:
125 | alert_users(context, update.effective_user, 'invalid query data', query.data)
126 | query.answer(text='Yo-he!', show_alert=True)
127 | return
128 | page = int(match[1])
129 | if fav_folder_ids := context.user_data.get(udkey_folders, None):
130 | page_data = [
131 | {
132 | 'text': simplified_path(fav_folder_ids[item]['path']),
133 | 'data': f'{item}',
134 | }
135 | for item in fav_folder_ids
136 | ]
137 |
138 | callback_query_prefix = 'save_to_folder'
139 |
140 | inline_keyboard_drive_ids = get_inline_keyboard_pagination_data(
141 | callback_query_prefix,
142 | page_data,
143 | page=page,
144 | max_per_page=10,
145 | )
146 | else:
147 | inline_keyboard_drive_ids = [[InlineKeyboardButton(text='🔹 If you don\'t have any shared drives, you must get one here : @MsGsuite before you can use this.', callback_data='#')]]
148 | inline_keyboard = inline_keyboard_drive_ids
149 | query.message.edit_reply_markup(reply_markup=InlineKeyboardMarkup(inline_keyboard))
150 |
151 |
152 | def save_to_folder(update, context):
153 | query = update.callback_query
154 | if query.message.chat_id < 0 and \
155 | (not query.message.reply_to_message or
156 | query.from_user.id != query.message.reply_to_message.from_user.id):
157 | alert_users(context, update.effective_user, 'invalid caller', query.data)
158 | query.answer(text='Yo-he!', show_alert=True)
159 | return
160 | match = re.search(r'^save_to_folder(?:_page#[\d]+)?,\s*([\dA-Za-z\-_]+)$', query.data)
161 | fav_folders = context.user_data.get(udkey_folders, {})
162 | if not match or match[1] not in fav_folders:
163 | alert_users(context, update.effective_user, 'invalid query', query.data)
164 | query.answer(text='Yo-he!', show_alert=True)
165 | return
166 | message = query.message
167 | text = message.caption or message.text
168 | folder_ids = parse_entity_for_drive_id(message)
169 |
170 | if not folder_ids:
171 | return
172 | dest_folder = fav_folders[match[1]]
173 | dest_folder['folder_id'] = match[1]
174 | if not thread_pool.get(update.effective_user.id, None):
175 | thread_pool[update.effective_user.id] = []
176 | t = MySaveFileThread(args=(update, context, folder_ids, text, dest_folder))
177 | thread_pool[update.effective_user.id].append(t)
178 | t.start()
179 | logger.debug(f'User {query.from_user.id} has added task {t.ident}.')
180 | query.message.edit_reply_markup(reply_markup=InlineKeyboardMarkup(
181 | [[InlineKeyboardButton(text='Executed', callback_data='#')]]))
182 |
--------------------------------------------------------------------------------
/telegram_gcloner/handlers/process_message.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import logging
4 |
5 | from telegram.ext import Dispatcher, MessageHandler, Filters, CallbackQueryHandler
6 |
7 | from handlers.process_drive_links import process_drive_links
8 | from utils.config_loader import config
9 | from utils.helper import parse_folder_id_from_url, alert_users
10 | from utils.process import leave_chat_from_message
11 | from utils.restricted import restricted_admin, restricted
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 |
16 | def init(dispatcher: Dispatcher):
17 | """Provide handlers initialization."""
18 | dispatcher.add_handler(
19 | MessageHandler(Filters.chat_type.groups & Filters.chat(config.GROUP_IDS) &
20 | (Filters.text | Filters.caption) &
21 | ~Filters.update.edited_message,
22 | process_message))
23 | dispatcher.add_handler(
24 | MessageHandler(Filters.chat(config.USER_IDS[0]) &
25 | (Filters.text | Filters.caption) &
26 | ~Filters.update.edited_message,
27 | process_message_from_authorised_user))
28 | dispatcher.add_handler(
29 | MessageHandler((~Filters.chat_type.groups) &
30 | (Filters.text | Filters.caption) &
31 | ~Filters.update.edited_message,
32 | process_message))
33 |
34 | dispatcher.add_handler(CallbackQueryHandler(ignore_callback, pattern=r'^#$'))
35 | dispatcher.add_handler(CallbackQueryHandler(get_warning))
36 |
37 |
38 | def ignore_callback(update, context):
39 | query = update.callback_query
40 | query.answer(text='')
41 |
42 |
43 | def get_warning(update, context):
44 | query = update.callback_query
45 | alert_users(context, update.effective_user, 'unknown query data', query.data)
46 | query.answer(text='Yo-he!', show_alert=True)
47 |
48 |
49 | def leave_from_chat(update, context):
50 | if update.channel_post:
51 | if update.channel_post.chat_id < 0 and update.channel_post.chat_id not in config.GROUP_IDS:
52 | leave_chat_from_message(update.channel_post, context)
53 | return
54 | elif update.message.chat_id < 0 and update.message.chat_id not in config.GROUP_IDS:
55 | leave_chat_from_message(update.message, context)
56 | return
57 |
58 |
59 | @restricted_admin
60 | def process_message_from_authorised_user(update, context):
61 | logger.debug(update.message)
62 | if update.message.caption:
63 | text_urled = update.message.caption_html_urled
64 | else:
65 | text_urled = update.message.text_html_urled
66 | if parse_folder_id_from_url(text_urled):
67 | process_drive_links(update, context)
68 | return
69 |
70 |
71 | @restricted
72 | def process_message(update, context):
73 | if not update.message:
74 | return
75 | if update.message.chat_id != config.USER_IDS[0]:
76 | logger.debug(update.message)
77 | if update.message.caption:
78 | text_urled = update.message.caption_html_urled
79 | else:
80 | text_urled = update.message.text_html_urled
81 | if parse_folder_id_from_url(text_urled):
82 | process_drive_links(update, context)
83 | return
84 |
--------------------------------------------------------------------------------
/telegram_gcloner/handlers/sa.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import configparser
4 | import datetime
5 | import logging
6 | import os
7 | import shutil
8 | import time
9 | from pathlib import Path
10 | from zipfile import ZipFile
11 |
12 | from telegram.ext import Dispatcher, CommandHandler, MessageHandler, Filters
13 |
14 | from utils.config_loader import config
15 | from utils.restricted import restricted_private
16 |
17 | logger = logging.getLogger(__name__)
18 |
19 |
20 | def init(dispatcher: Dispatcher):
21 | """Provide handlers initialization."""
22 | dispatcher.add_handler(CommandHandler('sa', get_sa, filters=~Filters.update.edited_message))
23 | dispatcher.add_handler(MessageHandler(Filters.chat_type.private & Filters.document, get_sa))
24 |
25 |
26 | @restricted_private
27 | def get_sa(update, context):
28 | instruction_text = 'Please private message a ZIP archive 🗂 containing SA files and write /sa in the subject.\n' \
29 | '📱 If you are using your phone, upload the ZIP archive first, then reply with /sa'
30 | if update.message and update.message.caption and update.message.caption.startswith('/sa'):
31 | document = update.message.document
32 | elif update.message and update.message.reply_to_message:
33 | document = update.message.reply_to_message.document
34 | else:
35 | update.message.reply_text(instruction_text)
36 | return
37 |
38 | if not document:
39 | update.message.reply_text(instruction_text)
40 | return
41 | gclone_path = os.path.join(config.BASE_PATH,
42 | 'gclone_config',
43 | str(update.effective_user.id))
44 | current_time = datetime.datetime.now()
45 | file_name = document.file_name
46 |
47 | if not file_name.endswith('zip'):
48 | update.message.reply_text('Only Zip Files are accepted.')
49 | return
50 |
51 | file_pah = os.path.join(
52 | gclone_path,
53 | f'{current_time.strftime("%Y-%m-%d")}_{current_time.strftime("%H-%M-%S")}_{file_name}',
54 | )
55 |
56 | if not os.path.isdir(gclone_path):
57 | Path(gclone_path).mkdir(parents=True, exist_ok=True)
58 | file = document.get_file(timeout=20)
59 | file.download(custom_path=file_pah)
60 |
61 | zip_path = os.path.join(gclone_path, 'current')
62 |
63 | # remove old files
64 | if os.path.isdir(zip_path):
65 | shutil.rmtree(zip_path)
66 | while os.path.exists(zip_path):
67 | time.sleep(1)
68 | Path(zip_path).mkdir(parents=True, exist_ok=True)
69 |
70 | # unzip files
71 | with ZipFile(file_pah, 'r') as zip_file:
72 | for member in zip_file.namelist():
73 | filename = os.path.basename(member)
74 | # skip directories
75 | if not filename:
76 | continue
77 |
78 | source = zip_file.open(member)
79 | target = open(os.path.join(zip_path, filename), "wb")
80 | with source, target:
81 | shutil.copyfileobj(source, target)
82 |
83 | # remove non json
84 | puppet_file = None
85 | json_count = 1
86 | for f in os.listdir(zip_path):
87 | current_file = os.path.join(zip_path, f)
88 | if not f.endswith('.json'):
89 | os.remove(current_file)
90 | elif not puppet_file:
91 | puppet_file = os.path.join(zip_path, 'google_drive_puppet.json')
92 | shutil.copy(current_file, puppet_file)
93 | else:
94 | json_count += 1
95 | if not puppet_file:
96 | update.message.reply_text(instruction_text)
97 | return
98 |
99 | # generate config file
100 | config_file = configparser.ConfigParser()
101 | config_file.add_section('gc')
102 | config_file.set('gc', 'type', 'drive')
103 | config_file.set('gc', 'scope', 'drive')
104 | config_file.set('gc', 'service_account_file', puppet_file)
105 | config_file.set('gc', 'service_account_file_path', zip_path + os.path.sep)
106 | config_file.set('gc', 'root_folder_id', 'root')
107 |
108 | with open(os.path.join(zip_path, 'rclone.conf'), 'w') as file_to_write:
109 | config_file.write(file_to_write)
110 |
111 | update.message.reply_text(
112 | f'✔️ A total of {json_count} SA files were received and configured to use in CloneBot V2. \n │ Now bookmark your favorite folders with /folders'
113 | )
114 |
115 | logger.info(
116 | f'{json_count} Service Accounts have been saved for the User {update.effective_user.id}.'
117 | )
118 |
--------------------------------------------------------------------------------
/telegram_gcloner/handlers/start.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import logging
4 |
5 | from telegram.ext import Dispatcher, CommandHandler
6 |
7 | from utils.callback import callback_delete_message
8 | from utils.config_loader import config
9 | from utils.restricted import restricted
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | def init(dispatcher: Dispatcher):
15 | """Provide handlers initialization."""
16 | dispatcher.add_handler(CommandHandler('start', start))
17 |
18 |
19 | @restricted
20 | def start(update, context):
21 | rsp = update.message.reply_text('🔺 First, send me a ZIP archive containing the SA files and add /sa to the subject. 🔺\n'
22 | '📂 After that, use /folders to set and mark/favourite your destination folders. 📂\n'
23 | '🔗 You are now ready to go! Just forward or send a Google Drive link to clone the File/Folder 🔗 \n.')
24 | rsp.done.wait(timeout=60)
25 | message_id = rsp.result().message_id
26 | if update.message.chat_id < 0:
27 | context.job_queue.run_once(callback_delete_message, config.TIMER_TO_DELETE_MESSAGE,
28 | context=(update.message.chat_id, message_id))
29 | context.job_queue.run_once(callback_delete_message, config.TIMER_TO_DELETE_MESSAGE,
30 | context=(update.message.chat_id, update.message.message_id))
31 |
--------------------------------------------------------------------------------
/telegram_gcloner/handlers/stop_task.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import logging
4 | import re
5 |
6 | from telegram.ext import Dispatcher, CallbackQueryHandler
7 |
8 | from utils.fire_save_files import thread_pool
9 | from utils.helper import alert_users
10 | from utils.restricted import restricted
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 | regex_stop_task = r'^stop_task,(\d+)'
15 |
16 |
17 | def init(dispatcher: Dispatcher):
18 | """Provide handlers initialization."""
19 | dispatcher.add_handler(CallbackQueryHandler(stop_task, pattern=regex_stop_task))
20 |
21 |
22 | @restricted
23 | def stop_task(update, context):
24 | query = update.callback_query
25 | if query.message.chat_id < 0 and \
26 | (not query.message.reply_to_message or
27 | query.from_user.id != query.message.reply_to_message.from_user.id):
28 | alert_users(context, update.effective_user, 'invalid caller', query.data)
29 | query.answer(text='Yo-he!', show_alert=True)
30 | return
31 | if query.data:
32 | if match := re.search(regex_stop_task, query.data):
33 | thread_id = int(match[1])
34 | if tasks := thread_pool.get(update.effective_user.id, None):
35 | for t in tasks:
36 | if t.ident == thread_id and t.owner == query.from_user.id:
37 | t.kill()
38 | logger.info(f'User {query.from_user.id} has stopped Cloning Task {thread_id}')
39 | return
40 | alert_users(context, update.effective_user, 'invalid query data', query.data)
41 | query.answer(text='Yo-he!', show_alert=True)
42 | return
43 |
--------------------------------------------------------------------------------
/telegram_gcloner/handlers/vip.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import copy
4 | import logging
5 |
6 | from telegram import Update
7 | from telegram.ext import Dispatcher, CommandHandler, CallbackContext, Filters
8 |
9 | from utils.config_loader import config
10 | from utils.restricted import restricted_admin
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | def init(dispatcher: Dispatcher):
16 | """Provide handlers initialization."""
17 | dispatcher.add_handler(CommandHandler('vip', vip, filters=Filters.chat(config.USER_IDS[0]), pass_args=True))
18 | dispatcher.add_handler(CommandHandler('unvip', unvip, filters=Filters.chat(config.USER_IDS[0]), pass_args=True))
19 |
20 |
21 | @restricted_admin
22 | def vip(update: Update, context: CallbackContext):
23 | if not context.args:
24 | if vip_list := context.bot_data.get('vip', None):
25 | update.message.reply_text('\n'.join(map(str, vip_list)))
26 | return
27 | if not context.args[0].isdigit:
28 | update.message.reply_text('/vip user_id')
29 | return
30 | user_id = int(context.args[0])
31 | if not context.bot_data.get('vip', None):
32 | context.bot_data['vip'] = [user_id]
33 | elif user_id not in context.bot_data['vip']:
34 | new_vip = copy.deepcopy(context.bot_data['vip'])
35 | new_vip.append(user_id)
36 | context.bot_data['vip'] = new_vip
37 | else:
38 | update.message.reply_text('User already Exists in VIP Users List.')
39 | return
40 | context.dispatcher.update_persistence()
41 | update.message.reply_text('User added successfully to VIP Users List.')
42 | logger.info(f'{user_id} is added successfully to VIP Users List.')
43 | return
44 |
45 |
46 | @restricted_admin
47 | def unvip(update: Update, context: CallbackContext):
48 | if not context.args or not context.args[0].isdigit:
49 | update.message.reply_text('/unvip user_id')
50 | return
51 | user_id = int(context.args[0])
52 | if user_id in context.bot_data.get('vip', []):
53 | new_vip = copy.deepcopy(context.bot_data['vip'])
54 | new_vip.remove(user_id)
55 | context.bot_data['vip'] = new_vip
56 | context.dispatcher.update_persistence()
57 | update.message.reply_text('Removed from VIP.')
58 | logger.info(f'{user_id} is successfully removed from VIP Users List.')
59 | else:
60 | update.message.reply_text('Could not find User in VIP Users List.')
61 |
62 | return
63 |
--------------------------------------------------------------------------------
/telegram_gcloner/telegram_gcloner.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import functools
4 | import logging
5 | import html
6 | import os.path
7 | import re
8 | import sys
9 |
10 | import traceback
11 | from importlib import import_module
12 | from logging import handlers
13 | from pathlib import Path
14 |
15 | from telegram import ParseMode
16 | from telegram.ext import Updater, Dispatcher
17 |
18 | import telegram.bot
19 | from telegram.ext import messagequeue as mq
20 | from telegram.ext.picklepersistence import PicklePersistence
21 |
22 | from telegram.utils.helpers import mention_html
23 | from telegram.utils.request import Request as TGRequest
24 |
25 |
26 | from utils.config_loader import config
27 |
28 |
29 | logger = logging.getLogger(__name__)
30 |
31 |
32 | class MQBot(telegram.bot.Bot):
33 | """A subclass of Bot which delegates send method handling to MQ"""
34 |
35 | def __init__(self, *args, is_queued_def=True, mqueue=None, **kwargs):
36 | super(MQBot, self).__init__(*args, **kwargs)
37 | # below 2 attributes should be provided for decorator usage
38 | self._is_messages_queued_default = is_queued_def
39 | self._msg_queue = mqueue or mq.MessageQueue(
40 | all_burst_limit=29,
41 | all_time_limit_ms=1017,
42 | group_burst_limit=19,
43 | group_time_limit_ms=60000,
44 | )
45 |
46 | def __del__(self):
47 | try:
48 | self._msg_queue.stop()
49 | except:
50 | pass
51 |
52 | def auto_group(method):
53 | @functools.wraps(method)
54 | def wrapped(self, *args, **kwargs):
55 | chat_id = 0
56 | if "chat_id" in kwargs:
57 | chat_id = kwargs["chat_id"]
58 | elif len(args) > 0:
59 | chat_id = args[0]
60 | is_group = True if type(chat_id) is str else (chat_id < 0)
61 | return method(self, *args, **kwargs, isgroup=is_group)
62 |
63 | @mq.queuedmessage
64 | def send_message(self, *args, **kwargs):
65 | """Wrapped method would accept new `queued` and `isgroup`
66 | OPTIONAL arguments"""
67 | return super(MQBot, self).send_message(*args, **kwargs)
68 |
69 | @mq.queuedmessage
70 | def send_photo(self, *args, **kwargs):
71 | """Wrapped method would accept new `queued` and `isgroup`
72 | OPTIONAL arguments"""
73 | return super(MQBot, self).send_photo(*args, **kwargs)
74 | #
75 | # @mq.queuedmessage
76 | # def edit_message_text(self, *args, **kwargs):
77 | # '''Wrapped method would accept new `queued` and `isgroup`
78 | # OPTIONAL arguments'''
79 | # return super(MQBot, self).edit_message_text(*args, **kwargs)
80 |
81 | @mq.queuedmessage
82 | def forward_message(self, *args, **kwargs):
83 | """Wrapped method would accept new `queued` and `isgroup`
84 | OPTIONAL arguments"""
85 | return super(MQBot, self).forward_message(*args, **kwargs)
86 | #
87 | # @mq.queuedmessage
88 | # def answer_callback_query(self, *args, **kwargs):
89 | # '''Wrapped method would accept new `queued` and `isgroup`
90 | # OPTIONAL arguments'''
91 | # return super(MQBot, self).answer_callback_query(*args, **kwargs)
92 |
93 | @mq.queuedmessage
94 | def leave_chat(self, *args, **kwargs):
95 | """Wrapped method would accept new `queued` and `isgroup`
96 | OPTIONAL arguments"""
97 | return super(MQBot, self).leave_chat(*args, **kwargs)
98 |
99 |
100 | def main():
101 | log_file = init_logger()
102 | config.load_config()
103 | config.LOG_FILE = log_file
104 |
105 | telegram_pickle = PicklePersistence(
106 | filename=f'pickle_{config.USER_IDS[0]}',
107 | store_bot_data=True,
108 | store_user_data=True,
109 | store_chat_data=False,
110 | )
111 |
112 | q = mq.MessageQueue()
113 | request = TGRequest(con_pool_size=8)
114 | my_bot = MQBot(config.TELEGRAM_TOKEN, request=request, mqueue=q)
115 | updater = Updater(bot=my_bot, use_context=True, persistence=telegram_pickle)
116 |
117 | updater.dispatcher.add_error_handler(error)
118 |
119 | load_handlers(updater.dispatcher)
120 |
121 | updater.start_polling()
122 | updater.bot.send_message(chat_id=config.USER_IDS[0], text='Welcome to CloneBot V2⚡.\n Let\'s clone some data to your Team Drives !')
123 | updater.idle()
124 |
125 |
126 | def init_logger():
127 | root_logger = logging.getLogger()
128 | root_logger.setLevel(logging.DEBUG)
129 | console_logger = logging.StreamHandler()
130 | console_logger.setLevel(logging.INFO)
131 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
132 | console_logger.setFormatter(formatter)
133 | root_logger.addHandler(console_logger)
134 |
135 | this_file_name = os.path.basename(os.path.splitext(os.path.basename(__file__))[0])
136 |
137 | Path('./logs/').mkdir(parents=True, exist_ok=True)
138 | logfile = f'./logs/{this_file_name}'
139 |
140 | file_logger = handlers.TimedRotatingFileHandler(logfile, encoding='utf-8', when='midnight')
141 | file_logger.suffix = "%Y-%m-%d.log"
142 | file_logger.extMatch = re.compile(r'^\d{4}-\d{2}-\d{2}\.log$')
143 | file_logger.setLevel(logging.DEBUG)
144 | file_logger.setFormatter(formatter)
145 | root_logger.addHandler(file_logger)
146 |
147 | logging.getLogger('googleapiclient').setLevel(logging.CRITICAL)
148 | logging.getLogger('googleapiclient.discover').setLevel(logging.CRITICAL)
149 | logging.getLogger('googleapiclient.discovery_cache').setLevel(logging.CRITICAL)
150 | logging.getLogger('google.auth.transport.requests').setLevel(logging.INFO)
151 |
152 | logging.getLogger('telegram.bot').setLevel(logging.INFO)
153 | logging.getLogger('telegram.ext.dispatcher').setLevel(logging.INFO)
154 | logging.getLogger('telegram.ext.updater').setLevel(logging.INFO)
155 | logging.getLogger('telegram.vendor.ptb_urllib3.urllib3.connectionpool').setLevel(logging.INFO)
156 | logging.getLogger('JobQueue').setLevel(logging.INFO)
157 |
158 | return logfile
159 |
160 |
161 | def load_handlers(dispatcher: Dispatcher):
162 | """Load handlers from files in a 'bot' directory."""
163 | base_path = os.path.join(os.path.dirname(__file__), 'handlers')
164 | files = os.listdir(base_path)
165 |
166 | for file_name in files:
167 | if file_name.endswith('.py'):
168 | handler_module, _ = os.path.splitext(file_name)
169 | if handler_module == 'process_message':
170 | continue
171 |
172 | module = import_module(f'.{handler_module}', 'handlers')
173 | module.init(dispatcher)
174 | logger.info(f'loaded handler module: {handler_module}')
175 | module = import_module('.process_message', 'handlers')
176 | module.init(dispatcher)
177 | logger.info('loaded handler module: process_message')
178 |
179 |
180 | def error(update, context):
181 | devs = [config.USER_IDS[0]]
182 | # """Log Errors caused by Updates."""
183 | # text = 'Update "{}" caused error: "{}"'.format(update, context.error)
184 | # logger.warning(text)
185 |
186 | # This traceback is created with accessing the traceback object from the sys.exc_info, which is returned as the
187 | # third value of the returned tuple. Then we use the traceback.format_tb to get the traceback as a string, which
188 | # for a weird reason separates the line breaks in a list, but keeps the linebreaks itself. So just joining an
189 | # empty string works fine.
190 | trace = "".join(traceback.format_tb(sys.exc_info()[2]))
191 | # lets try to get as much information from the telegram update as possible
192 | payload = ""
193 | # normally, we always have an user. If not, its either a channel or a poll update.
194 | if update.effective_user:
195 | payload += f' with the user ' \
196 | f'{mention_html(update.effective_user.id, html.escape(update.effective_user.first_name))} '
197 | # there are more situations when you don't get a chat
198 | if update.effective_chat:
199 | if update.effective_chat.title:
200 | payload += f' within the chat {html.escape(update.effective_chat.title)}'
201 | if update.effective_chat.username:
202 | payload += f' (@{update.effective_chat.username}, {update.effective_chat.id})'
203 | # but only one where you have an empty payload by now: A poll (buuuh)
204 | if update.poll:
205 | payload += f' with the poll id {update.poll.id}.'
206 |
207 | context_error = str(context.error)
208 | # lets put this in a "well" formatted text
209 | text = f"Hey.\n The error {html.escape(context_error)}
happened{str(payload)}. The full traceback:\n\n{html.escape(trace)}
"
210 |
211 |
212 | # ignore message is not modified error from telegram
213 | if 'Message is not modified' in context_error:
214 | return
215 |
216 | # and send it to the dev(s)
217 | for dev_id in devs:
218 | context.bot.send_message(dev_id, text, parse_mode=ParseMode.HTML)
219 | # we raise the error again, so the logger module catches it. If you don't use the logger module, use it.
220 | raise
221 |
222 |
223 | if __name__ == '__main__':
224 | main()
225 |
--------------------------------------------------------------------------------
/telegram_gcloner/utils/callback.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import logging
4 |
5 | from telegram.ext import CallbackContext
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | def callback_delete_message(context: CallbackContext):
11 | (chat_id, message_id) = context.job.context
12 | try:
13 | context.bot.delete_message(chat_id=chat_id, message_id=message_id)
14 | except Exception as e:
15 | logger.warning(f'Could not delete message {message_id}: {e}')
16 |
--------------------------------------------------------------------------------
/telegram_gcloner/utils/config_loader.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import configparser
5 | import logging
6 | import os
7 | import shutil
8 | import sys
9 |
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | class _Config:
15 | def __init__(self):
16 | self._ad_string = ''
17 | self._log_file = ''
18 | self._telegram_token = None
19 | self._path_to_gclone = None
20 | self._user_ids = ''
21 | self._group_ids = ''
22 | self._gclone_para_override = ''
23 | self._base_path = os.path.dirname(os.path.dirname(__file__))
24 | self.TIMER_TO_DELETE_MESSAGE = 20
25 | self.AD_STRING = ' Goodbye, Please talk to the Bot privately.'
26 |
27 | def load_config(self):
28 | logger.debug('Loading config')
29 |
30 | try:
31 | config_file = configparser.ConfigParser(allow_no_value=True)
32 | config_file.read(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.ini'), encoding='utf-8')
33 | except IOError as err:
34 | logger.warning("Can't open the config file: ", err)
35 | input('Press enter to exit.')
36 | sys.exit(1)
37 |
38 | if not config_file.has_section('General'):
39 | logger.warning("Can't find General section in the Config File.")
40 | input('Press enter to exit.')
41 | sys.exit(1)
42 |
43 | config_general = config_file['General']
44 |
45 | config_general_keywords_str = [
46 | 'telegram_token',
47 | 'user_ids',
48 | 'group_ids',
49 | ]
50 |
51 | self.get_config_from_section('str', config_general_keywords_str, config_general)
52 | self.get_config_from_section('str', ['path_to_gclone', 'gclone_para_override'], config_general, optional=True)
53 |
54 | self._user_ids = [int(item) for item in self._user_ids.split(',')]
55 | self._group_ids = [int(item) for item in self._group_ids.split(',')]
56 |
57 | if not os.path.isfile(self._path_to_gclone):
58 | self._path_to_gclone = shutil.which('gclone')
59 | if not self._path_to_gclone:
60 | logger.warning('Gclone Executable was not found in the Drectory.')
61 | input("Press Enter to continue...")
62 | sys.exit(0)
63 | logger.info(f'Found gclone: {self._path_to_gclone}')
64 |
65 | if not self._telegram_token:
66 | logger.warning('Telegram Bot Token not found.')
67 | input("Press Enter to continue...")
68 | sys.exit(0)
69 | logger.info(f'Found Bot Token: {self._telegram_token}')
70 |
71 | if self._gclone_para_override:
72 | self._gclone_para_override = self._gclone_para_override.split()
73 |
74 | def get_config_from_section(self, var_type, keywords, section, optional=False):
75 | for item in keywords:
76 | if var_type == 'int':
77 | value = section.getint(item, 0)
78 | elif var_type == 'str':
79 | value = section.get(item, '')
80 | elif var_type == 'bool':
81 | value = section.getboolean(item, False)
82 | else:
83 | raise TypeError
84 | if not optional and not value and value is not False:
85 | logger.warning(f'{item} is not provided.')
86 | input("Press Enter to continue...")
87 | sys.exit(1)
88 | logger.info(f'Found {item}: {value}')
89 | setattr(self, f'_{item}', value)
90 |
91 | @property
92 | def PATH_TO_GCLONE(self):
93 | return self._path_to_gclone
94 |
95 | @property
96 | def TELEGRAM_TOKEN(self):
97 | return self._telegram_token
98 |
99 | @property
100 | def USER_IDS(self):
101 | return self._user_ids
102 |
103 | @property
104 | def GROUP_IDS(self):
105 | return self._group_ids
106 |
107 | @property
108 | def GCLONE_PARA_OVERRIDE(self):
109 | return self._gclone_para_override
110 |
111 | @property
112 | def BASE_PATH(self):
113 | return self._base_path
114 |
115 | @property
116 | def LOG_FILE(self):
117 | return self._log_file
118 |
119 | @LOG_FILE.setter
120 | def LOG_FILE(self, val):
121 | self._log_file = val
122 |
123 |
124 | config = _Config()
--------------------------------------------------------------------------------
/telegram_gcloner/utils/fire_save_files.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import datetime
4 | import html
5 | import logging
6 | import os
7 | import re
8 | import subprocess
9 | import threading
10 |
11 | from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton
12 |
13 | from utils.config_loader import config
14 | from utils.google_drive import GoogleDrive
15 |
16 | logger = logging.getLogger(__name__)
17 |
18 |
19 | thread_pool = {}
20 |
21 |
22 | class MySaveFileThread(threading.Thread):
23 | def __init__(self, args=(), kwargs=None):
24 | threading.Thread.__init__(self, args=(), kwargs=None)
25 | self.daemon = True
26 | self.args = args
27 | self.critical_fault = False
28 | self.owner = -1
29 |
30 | def run(self):
31 | update, context, folder_ids, text, dest_folder = self.args
32 | self.owner = update.effective_user.id
33 | thread_id = self.ident
34 | is_multiple_ids = len(folder_ids) > 1
35 | is_fclone = 'fclone' in os.path.basename(config.PATH_TO_GCLONE)
36 | chat_id = update.effective_chat.id
37 | user_id = update.effective_user.id
38 | gd = GoogleDrive(user_id)
39 | message = '╭──────⌈ 📥 Copying In Progress ⌋──────╮\n│\n├ 📂 Target Directory:{}\n'.format(dest_folder['path'])
40 | inline_keyboard = InlineKeyboardMarkup(
41 | [[InlineKeyboardButton(text=f'🚫 Stop', callback_data=f'stop_task,{thread_id}')]])
42 |
43 | reply_message_id = update.callback_query.message.reply_to_message.message_id \
44 | if update.callback_query.message.reply_to_message else None
45 | rsp = context.bot.send_message(chat_id=chat_id, text=message,
46 | parse_mode=ParseMode.HTML,
47 | disable_web_page_preview=True,
48 | reply_to_message_id=reply_message_id,
49 | reply_markup=inline_keyboard)
50 | rsp.done.wait(timeout=60)
51 | message_id = rsp.result().message_id
52 |
53 | for folder_id in folder_ids:
54 | destination_path = folder_ids[folder_id]
55 |
56 | command_line = [
57 | config.PATH_TO_GCLONE,
58 | 'copy',
59 | '--drive-server-side-across-configs',
60 | '-P',
61 | '--stats',
62 | '1s',
63 | '--ignore-existing'
64 | ]
65 | if config.GCLONE_PARA_OVERRIDE:
66 | command_line.extend(config.GCLONE_PARA_OVERRIDE)
67 | elif is_fclone is True:
68 | command_line += [
69 | '--checkers=256',
70 | '--transfers=256',
71 | '--drive-pacer-min-sleep=1ms',
72 | '--drive-pacer-burst=5000',
73 | '--check-first'
74 | ]
75 | else:
76 | command_line += [
77 | '--transfers',
78 | '8',
79 | '--tpslimit',
80 | '6',
81 | ]
82 | gclone_config = os.path.join(config.BASE_PATH,
83 | 'gclone_config',
84 | str(update.effective_user.id),
85 | 'current',
86 | 'rclone.conf')
87 | command_line += ['--config', gclone_config]
88 | command_line += [
89 | '{}:{{{}}}'.format('gc', folder_id),
90 | ('{}:{{{}}}/{}'.format('gc', dest_folder['folder_id'], destination_path))
91 | ]
92 |
93 | logger.debug('command line: ' + str(command_line))
94 |
95 | process = subprocess.Popen(command_line,
96 | bufsize=1,
97 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
98 | encoding='utf-8',
99 | errors='ignore',
100 | universal_newlines=True)
101 | progress_checked_files = 0
102 | progress_total_check_files = 0
103 | progress_transferred_file = 0
104 | progress_total_files = 0
105 | progress_file_percentage = 0
106 | progress_file_percentage_10 = 0
107 | progress_transferred_size = '0'
108 | progress_total_size = '0 Bytes'
109 | progress_speed = '-'
110 | progress_speed_file = '-'
111 | progress_eta = '-'
112 | progress_size_percentage_10 = 0
113 | regex_checked_files = r'Checks:\s+(\d+)\s+/\s+(\d+)'
114 | regex_total_files = r'Transferred:\s+(\d+) / (\d+), (\d+)%(?:,\s*([\d.]+\sFiles/s))?'
115 | regex_total_size = r'Transferred:[\s]+([\d.]+\s*[kMGTP]?) / ([\d.]+[\s]?[kMGTP]?Bytes),' \
116 | r'\s*(?:\-|(\d+)\%),\s*([\d.]+\s*[kMGTP]?Bytes/s),\s*ETA\s*([\-0-9hmsdwy]+)'
117 | message_progress_last = ''
118 | message_progress = ''
119 | progress_update_time = datetime.datetime.now() - datetime.timedelta(minutes=5)
120 | while True:
121 | try:
122 | line = process.stdout.readline()
123 | except Exception as e:
124 | logger.debug(str(e))
125 | if process.poll() is not None:
126 | break
127 | else:
128 | continue
129 | if not line and process.poll() is not None:
130 | break
131 | output = line.rstrip()
132 | if output:
133 | # logger.debug(output)
134 | match_total_files = re.search(regex_total_files, output)
135 | if match_total_files:
136 | progress_transferred_file = int(match_total_files.group(1))
137 | progress_total_files = int(match_total_files.group(2))
138 | progress_file_percentage = int(match_total_files.group(3))
139 | progress_file_percentage_10 = progress_file_percentage // 10
140 | if match_total_files.group(4):
141 | progress_speed_file = match_total_files.group(4)
142 | match_total_size = re.search(regex_total_size, output)
143 | if match_total_size:
144 | progress_transferred_size = match_total_size.group(1)
145 | progress_total_size = match_total_size.group(2)
146 | progress_size_percentage = int(match_total_size.group(3)) if match_total_size.group(
147 | 3) else 0
148 | progress_size_percentage_10 = progress_size_percentage // 10
149 | progress_speed = match_total_size.group(4)
150 | progress_eta = match_total_size.group(5)
151 | match_checked_files = re.search(regex_checked_files, output)
152 | if match_checked_files:
153 | progress_checked_files = int(match_checked_files.group(1))
154 | progress_total_check_files = int(match_checked_files.group(2))
155 | progress_max_percentage_10 = max(progress_size_percentage_10, progress_file_percentage_10)
156 | message_progress = '├──────⌈ Progress Details⌋──────' \
157 | '├ 🗂 Source : {}\n│\n' \
158 | '├ ✔️ Checks: {} / {}
\n' \
159 | '├ 📥 Transfers: {} / {}
\n' \
160 | '├ 📦 Size:{} / {}
\n{}' \
161 | '├ ⚡️Speed:{}
\n├⏳ ETA: {}
\n' \
162 | '├ ⛩ Progress:[{}
] {: >2}%\n│\n' \
163 | '├──────⌈ CloneBot V2🔥 ⌋──────' \
164 | .format(
165 | folder_id,
166 | html.escape(destination_path),
167 | progress_checked_files,
168 | progress_total_check_files,
169 | progress_transferred_file,
170 | progress_total_files,
171 | progress_transferred_size,
172 | progress_total_size,
173 | f'Speed:{progress_speed_file}
\n' if is_fclone is True else '',
174 | progress_speed,
175 | progress_eta,
176 | '●' * progress_file_percentage_10 + '○' * (
177 | progress_max_percentage_10 - progress_file_percentage_10) + ' ' * (
178 | 10 - progress_max_percentage_10),
179 | progress_file_percentage)
180 |
181 | match = re.search(r'Failed to Copy: Failed to Make Directory in the Destination', output)
182 | if match:
183 | message_progress = '{}\n│Destination Write Permission Error.\n Please ensure that you have rights to upload files to the Destination.
'.format(message_progress)
184 | temp_message = '{}{}'.format(message, message_progress)
185 | # logger.info('Write permission error, please confirm permission'.format())
186 | try:
187 | context.bot.edit_message_text(chat_id=chat_id, message_id=message_id,
188 | text=temp_message, parse_mode=ParseMode.HTML,
189 | disable_web_page_preview=True,
190 | reply_markup=inline_keyboard)
191 | except Exception as e:
192 | logger.debug('Error {} occurs when editing message {} for user {} in chat {}: \n│{}'.format(
193 | e, message_id, user_id, chat_id, temp_message))
194 | process.terminate()
195 | self.critical_fault = True
196 | break
197 |
198 | match = re.search(r"Couldn't List Directory", output)
199 | if match:
200 | message_progress = '{}\n│Source Read permission Error. \n Please ensure that you have rights to read files from the Source Link
'.format(message_progress)
201 | temp_message = '{}{}'.format(message, message_progress)
202 | # logger.info('Read permission error, please confirm the permission:')
203 | try:
204 | context.bot.edit_message_text(chat_id=chat_id, message_id=message_id,
205 | text=temp_message, parse_mode=ParseMode.HTML,
206 | disable_web_page_preview=True,
207 | reply_markup=inline_keyboard)
208 | except Exception as e:
209 | logger.debug('Error {} occurs when editing message {} for user {} in chat {}: \n│{}'.format(
210 | e, message_id, user_id, chat_id, temp_message))
211 | process.terminate()
212 | self.critical_fault = True
213 | break
214 |
215 | if message_progress != message_progress_last:
216 | if datetime.datetime.now() - progress_update_time > datetime.timedelta(seconds=5):
217 | temp_message = '{}{}'.format(message, message_progress)
218 | try:
219 | context.bot.edit_message_text(chat_id=chat_id, message_id=message_id,
220 | text=temp_message, parse_mode=ParseMode.HTML,
221 | disable_web_page_preview=True,
222 | reply_markup=inline_keyboard)
223 | except Exception as e:
224 | logger.debug(
225 | 'Error {} occurs when editing message {} for user {} in chat {}: \n│{}'.format(
226 | e, message_id, user_id, chat_id, temp_message))
227 | message_progress_last = message_progress
228 | progress_update_time = datetime.datetime.now()
229 |
230 | if self.critical_fault:
231 | message_progress = '{}\n│\n│ You have terminated the Cloning Process'.format(message_progress)
232 | process.terminate()
233 | break
234 |
235 | rc = process.poll()
236 | message_progress_heading, message_progress_content = message_progress.split('\n│', 1)
237 | link_text = 'Unable to fetch Google Drive Link.'
238 | try:
239 | link = gd.get_folder_link(dest_folder['folder_id'], destination_path)
240 | if link:
241 | link_text = '\n│ \n│ 👉 Google Drive Link 👈'.format(link)
242 | except Exception as e:
243 | logger.info(str(e))
244 |
245 | if self.critical_fault is True:
246 | message = '{}{} ❌\n│{}\n│{}\n│'.format(message, message_progress_heading, message_progress_content,
247 | link_text)
248 | elif progress_file_percentage == 0 and progress_checked_files > 0:
249 | message = '{}{} ✅\n│ File Already Exists in the Destination!\n│ {}\n│'.format(message, message_progress_heading, link_text)
250 | else:
251 | message = '{}{}{}\n│{}\n│{}\n│\n│'.format(message,
252 | message_progress_heading,
253 | '✅' if rc == 0 else '❌',
254 | message_progress_content,
255 | link_text)
256 |
257 | try:
258 | context.bot.edit_message_text(chat_id=chat_id, message_id=message_id, text=message,
259 | parse_mode=ParseMode.HTML, disable_web_page_preview=True,
260 | reply_markup=inline_keyboard)
261 | except Exception as e:
262 | logger.debug('Error {} occurs when editing message {} for user {} in chat {}: \n│{}'.format(
263 | e, message_id, user_id, chat_id, message))
264 |
265 | if self.critical_fault is True:
266 | break
267 |
268 | message += '\n╰──────⌈ ✅ Cloning Process Finished ! ✅ ⌋──────╯'
269 | try:
270 | context.bot.edit_message_text(chat_id=chat_id, message_id=message_id, text=message,
271 | parse_mode=ParseMode.HTML, disable_web_page_preview=True)
272 | except Exception as e:
273 | logger.debug('Error {} occurs when editing message {} for user {} in chat {}: \n│{}'.format(
274 | e, message_id, user_id, chat_id, message))
275 | update.callback_query.message.edit_reply_markup(reply_markup=InlineKeyboardMarkup(
276 | [[InlineKeyboardButton(text='Done', callback_data='cancel')]]))
277 |
278 | logger.debug('User {} has finished task {}: \n│{}'.format(user_id, thread_id, message))
279 | tasks = thread_pool.get(user_id, None)
280 | if tasks:
281 | for t in tasks:
282 | if t.ident == thread_id:
283 | tasks.remove(t)
284 | return
285 |
286 | def kill(self):
287 | self.critical_fault = True
288 |
--------------------------------------------------------------------------------
/telegram_gcloner/utils/google_drive.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import copy
4 | import logging
5 | import os
6 |
7 | from googleapiclient import errors
8 | from googleapiclient.discovery import build
9 | from google.auth.transport.requests import Request
10 | from google.oauth2 import service_account
11 |
12 | from utils.config_loader import config
13 |
14 | logger = logging.getLogger(__name__)
15 |
16 |
17 | class GoogleDrive:
18 | def __init__(self, user_id):
19 | service_account_file = os.path.join(config.BASE_PATH,
20 | 'gclone_config',
21 | str(user_id),
22 | 'current',
23 | 'google_drive_puppet.json')
24 |
25 | creds = None
26 | scopes = ['https://www.googleapis.com/auth/drive']
27 |
28 | if os.path.exists(service_account_file):
29 | creds = service_account.Credentials.from_service_account_file(
30 | service_account_file, scopes=scopes)
31 | if not creds.valid:
32 | creds.refresh(Request())
33 |
34 | # If there are no (valid) credentials available, throw error.
35 | if not creds or not creds.valid:
36 | raise FileNotFoundError
37 |
38 | self.service = build('drive', 'v3', credentials=creds)
39 |
40 | def get_drives(self):
41 | result = []
42 | page_token = None
43 | while True:
44 | try:
45 | param = {
46 | 'pageSize': 100,
47 | }
48 | if page_token:
49 | param['pageToken'] = page_token
50 | drives = self.service.drives().list(**param).execute()
51 |
52 | result.extend(drives['drives'])
53 | logger.debug(f"Received {len(drives['drives'])} drives")
54 | page_token = drives.get('nextPageToken')
55 | if not page_token:
56 | break
57 | except errors.HttpError as error:
58 | logger.warning(f'An error occurred: {error}')
59 | break
60 | return {item['id']: item['name'] for item in result}
61 |
62 | def get_file_name(self, file_id):
63 | param = {
64 | 'fileId': file_id,
65 | 'supportsAllDrives': True,
66 | 'fields': 'name, driveId',
67 | }
68 | file_info = self.service.files().get(**param).execute()
69 | file_name = file_info['name']
70 | if file_info.get('driveId', None) == file_id:
71 | file_name = self.get_drive_name(file_id)
72 | return file_name
73 |
74 | def get_file_path_from_id(self, file_id, parents=[]):
75 | result = copy.deepcopy(parents)
76 | param = {
77 | 'fileId': file_id,
78 | 'supportsAllDrives': True,
79 | 'fields': 'name, mimeType, parents, driveId',
80 | }
81 | file_info = self.service.files().get(**param).execute()
82 | if file_info.get('driveId', None) == file_id:
83 | drive_name = self.get_drive_name(file_id)
84 | parent_entry = {'name': drive_name, 'folder_id': file_id}
85 | else:
86 | parent_entry = {'name': file_info['name'], 'folder_id': file_id}
87 | parent = file_info.get('parents', None)
88 | result.append(parent_entry)
89 | if parent:
90 | return self.get_file_path_from_id(parent[0], result)
91 | logger.debug(str(result))
92 | return result
93 |
94 | def get_drive_name(self, drive_id):
95 | param = {
96 | 'driveId': drive_id,
97 | }
98 | drive_info = self.service.drives().get(**param).execute()
99 | return drive_info['name']
100 |
101 | def list_folders(self, folder_id):
102 | result = []
103 |
104 | page_token = None
105 | while True:
106 | try:
107 | param = {
108 | 'q': f"'{folder_id}' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false",
109 | 'includeItemsFromAllDrives': True,
110 | 'supportsAllDrives': True,
111 | 'fields': 'nextPageToken, files(id, name)',
112 | 'pageSize': 1000,
113 | }
114 |
115 | if page_token:
116 | param['pageToken'] = page_token
117 | all_files = self.service.files().list(**param).execute()
118 |
119 | result.extend(all_files['files'])
120 | logger.debug(f"Received {len(all_files['files'])} files")
121 | page_token = all_files.get('nextPageToken')
122 |
123 | if not page_token:
124 | break
125 | except errors.HttpError as error:
126 | logger.info(f'An error occurred: {error}')
127 | break
128 | result_sorted = sorted(result, key=lambda k: k['name'])
129 | return {item['id']: item['name'] for item in result_sorted}
130 |
131 | def get_folder_link(self, folder_id, folder_path):
132 | folder_path_list = list(filter(None, folder_path.split('/')))
133 | if result := self.get_folder_id_by_name(folder_id, folder_path_list[0]):
134 | if len(folder_path_list) > 1:
135 | for item in result:
136 | next_result = self.get_folder_link(item['id'], '/'.join(folder_path_list[1:]))
137 | if isinstance(next_result, str):
138 | return next_result
139 | return None
140 | else:
141 | link = f"https://drive.google.com/open?id={result[0]['id']}"
142 | logger.info(f'found link: {link}')
143 | return link
144 | return None
145 |
146 | def get_folder_id_by_name(self, folder_id, folder_name):
147 | page_token = None
148 | result = []
149 | while True:
150 | try:
151 | param = {
152 | 'q': f"name = '{folder_name}' and mimeType = 'application/vnd.google-apps.folder' and '{folder_id}' in parents and trashed = false",
153 | 'includeItemsFromAllDrives': True,
154 | 'supportsAllDrives': True,
155 | 'fields': 'nextPageToken, files(id, name)',
156 | 'pageSize': 1000,
157 | }
158 |
159 | if page_token:
160 | param['pageToken'] = page_token
161 | # logger.debug(str(param))
162 | all_files = self.service.files().list(**param).execute()
163 |
164 | result.extend(all_files['files'])
165 | # logger.debug(str(allFiles))
166 | # logger.info('Received {} files'.format(len(allFiles['files'])))
167 | page_token = all_files.get('nextPageToken')
168 |
169 | if not page_token:
170 | break
171 | except errors.HttpError as error:
172 | logger.info(f'An error occurred: {error}')
173 | break
174 | return result
175 |
--------------------------------------------------------------------------------
/telegram_gcloner/utils/helper.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import html
4 | import logging
5 | import math
6 | import re
7 |
8 | from telegram import ParseMode, InlineKeyboardButton
9 | from telegram.utils.helpers import mention_html
10 |
11 | from utils.config_loader import config
12 |
13 | logger = logging.getLogger(__name__)
14 |
15 |
16 | def parse_folder_id_from_url(url):
17 | folder_id = None
18 |
19 | pattern = r'https://drive\.google\.com/(?:' \
20 | r'drive/(?:u/[\d]+/)?(?:mobile/)?folders/([\w.\-_]+)(?:\?[\=\w]+)?|' \
21 | r'folderview\?id=([\w.\-_]+)(?:\&[=\w]+)?|' \
22 | r'open\?id=([\w.\-_]+)(?:\&[=\w]+)?|' \
23 | r'(?:a/[\w.\-_]+/)?file/d/([\w.\-_]+)|' \
24 | r'(?:a/[\w.\-_]+/)?uc\?id\=([\w.\-_]+)&?' \
25 | r')'
26 |
27 | if x := re.search(pattern, url):
28 | folder_id = ''.join(filter(None, x.groups()))
29 |
30 | if folder_id:
31 | logger.debug(f'folder_id: {folder_id}')
32 | return folder_id
33 |
34 |
35 | def alert_users(context, user_info, warning_message, text):
36 | mention_html_user = mention_html(user_info.id, html.escape(user_info.full_name))
37 | message = f'🤔 Detected Suspicious Behaviour from User {mention_html_user} {user_info.id}: {warning_message} {text}.'
38 |
39 | logger.info(message)
40 | context.bot.send_message(chat_id=config.USER_IDS[0], text=message, parse_mode=ParseMode.HTML)
41 |
42 |
43 | def get_inline_keyboard_pagination_data(callback_query_prefix, page_data, page_data_chosen=None, page=1,
44 | max_per_page=10):
45 | callback_query_prefix_data = f'{callback_query_prefix}_page#{page}'
46 | page_data_len = len(page_data)
47 | total_page = math.ceil(page_data_len / max_per_page)
48 | inline_keyboard = []
49 | for i in range(max((min(page, total_page) - 1) * max_per_page, 0),
50 | min(max(page, 1) * max_per_page, page_data_len)):
51 | if isinstance(page_data[i], list):
52 | inline_keyboard_row = []
53 | for j in range(len(page_data[i])):
54 | is_chosen = any(k == page_data[i][j]['data'] for k in page_data_chosen or [])
55 | text = f"{'✅ ' if is_chosen else ''}{page_data[i][j]['text']}"
56 | if page_data[i][j]['data'] != '#':
57 | data = f"{f'un{callback_query_prefix_data}' if is_chosen else callback_query_prefix_data},{page_data[i][j]['data']}"
58 |
59 | else:
60 | data = '#'
61 | inline_keyboard_row.append(InlineKeyboardButton(text, callback_data=data))
62 | inline_keyboard.append(inline_keyboard_row)
63 | else:
64 | is_chosen = any(k == page_data[i]['data'] for k in page_data_chosen or [])
65 | text = f"{'✅ ' if is_chosen else ''}{page_data[i]['text']}"
66 | if page_data[i]['data'] != '#':
67 | data = f"{f'un{callback_query_prefix_data}' if is_chosen else callback_query_prefix_data},{page_data[i]['data']}"
68 |
69 | else:
70 | data = '#'
71 | inline_keyboard.append(
72 | [InlineKeyboardButton(text, callback_data=data)])
73 | if total_page > 1:
74 | inline_keyboard.extend(get_inline_keyboard_pagination_paginator(callback_query_prefix,
75 | total_page,
76 | page=page,
77 | ))
78 | return inline_keyboard
79 |
80 |
81 | def get_inline_keyboard_pagination_paginator(callback_query_prefix, total_page, page=1, total_pages_shown=5):
82 | start_page = min(max(page - total_pages_shown // 2, 1), max(total_page - total_pages_shown + 1, 1))
83 | inline_keyboard_pagination_page = [
84 | InlineKeyboardButton(
85 | f'{i}' if i != page else f'*{i}',
86 | callback_data=f'{callback_query_prefix}_page#{i}'
87 | if i != page
88 | else '#',
89 | )
90 | for i in range(
91 | start_page, min(start_page + total_pages_shown, total_page + 1)
92 | )
93 | ]
94 |
95 | inline_keyboard_pagination = [inline_keyboard_pagination_page]
96 | if total_page > total_pages_shown:
97 | previous_1 = max(page - 1, 1)
98 | previous_2 = max(page - total_pages_shown, 1)
99 | next_1 = min(page + 1, total_page)
100 | next_2 = min(page + total_pages_shown, total_page)
101 |
102 | inline_keyboard_pagination_nav = [
103 | InlineKeyboardButton(
104 | '|<',
105 | callback_data=f'{callback_query_prefix}_page#1'
106 | if page != 1
107 | else '#',
108 | ),
109 | InlineKeyboardButton(
110 | '<<',
111 | callback_data=f'{callback_query_prefix}_page#{previous_2}'
112 | if page != previous_2
113 | else '#',
114 | ),
115 | InlineKeyboardButton(
116 | '<',
117 | callback_data=f'{callback_query_prefix}_page#{previous_1}'
118 | if page != previous_1
119 | else '#',
120 | ),
121 | InlineKeyboardButton(f'{page}/{total_page}', callback_data='#'),
122 | InlineKeyboardButton(
123 | '>',
124 | callback_data=f'{callback_query_prefix}_page#{next_1}'
125 | if page != next_1
126 | else '#',
127 | ),
128 | InlineKeyboardButton(
129 | '>>',
130 | callback_data=f'{callback_query_prefix}_page#{next_2}'
131 | if page != next_2
132 | else '#',
133 | ),
134 | InlineKeyboardButton(
135 | '>|',
136 | callback_data=f'{callback_query_prefix}_page#{total_page}'
137 | if page != total_page
138 | else '#',
139 | ),
140 | ]
141 |
142 | inline_keyboard_pagination.append(inline_keyboard_pagination_nav)
143 | return inline_keyboard_pagination
144 |
145 |
146 | def simplified_path(folder_path):
147 | max_length = 30
148 |
149 | prefix, delimiter, postfix = folder_path.rpartition('/')
150 | spare_length = max(max_length - len(postfix), 0)
151 |
152 | # logger.debug(prefix)
153 | return f"{f'{prefix[:spare_length]}..' if len(prefix) > spare_length else prefix}/{postfix}"
154 |
--------------------------------------------------------------------------------
/telegram_gcloner/utils/process.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import html
4 | import logging
5 |
6 |
7 | from telegram import ParseMode
8 | from telegram.utils.helpers import mention_html
9 |
10 | from utils.config_loader import config
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | def leave_chat_from_message(message, context):
16 | context.bot.send_message(
17 | chat_id=message.chat_id,
18 | text=f'Hey, Thank you for adding CloneBot V2 to this group. {config.AS_STRING.format(context.bot.username)}',
19 | parse_mode=ParseMode.HTML,
20 | )
21 |
22 | context.bot.send_message(chat_id=message.chat_id, text='\n\nUnfortunately I am not authorized in this Group/Chat 😔 \n So I am leavng this Group \nIf you want me in this Group/Chat, ask my owner to authorize me here 😉.')
23 | if message.from_user:
24 | mention_html_from_user = mention_html(message.from_user.id,
25 | message.from_user.full_name.full_name)
26 | text = f'🔙 Left Unauthorized Group : \n │ Name : {html.escape(message.chat.title)} ({message.chat_id}). \n │ Added by : {mention_html_from_user} {message.from_user.id}. \n │ Message : {message.text}'
27 |
28 | else:
29 | text = f'🔙 Left Unauthorized Group : \n │ Name : {html.escape(message.chat.title)} ({message.chat_id}). \n │ Message : {message.text}'
30 |
31 | context.bot.leave_chat(message.chat_id)
32 | logger.warning(text)
33 | context.bot.send_message(chat_id=config.USER_IDS[0], text=text, parse_mode=ParseMode.HTML)
34 |
--------------------------------------------------------------------------------
/telegram_gcloner/utils/restricted.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import logging
4 | from functools import wraps
5 |
6 | from utils.callback import callback_delete_message
7 | from utils.config_loader import config
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | def restricted(func):
13 | @wraps(func)
14 | def wrapped(update, context, *args, **kwargs):
15 | if not update.effective_user:
16 | return
17 | user_id = update.effective_user.id
18 | ban_list = context.bot_data.get('ban', [])
19 | # access control. comment out one or the other as you wish. otherwise you can use any of the following examples.
20 | # if user_id in ban_list:
21 | if user_id in ban_list or user_id not in config.USER_IDS:
22 | logger.info('UnAuthorized Access denied for {} {}.'
23 | .format(update.effective_user.full_name, user_id))
24 | return
25 | return func(update, context, *args, **kwargs)
26 | return wrapped
27 |
28 |
29 | def restricted_private(func):
30 | @wraps(func)
31 | def wrapped(update, context, *args, **kwargs):
32 | if not update.effective_user:
33 | return
34 | user_id = update.effective_user.id
35 | chat_id = update.effective_chat.id
36 | ban_list = context.bot_data.get('ban', [])
37 | if user_id in ban_list or chat_id < 0:
38 | logger.info('Unauthorized access denied for private messages {} {}.'
39 | .format(update.effective_user.full_name, user_id))
40 | if chat_id < 0:
41 | rsp = update.message.reply_text('Private chat only!')
42 | rsp.done.wait(timeout=60)
43 | message_id = rsp.result().message_id
44 | context.job_queue.run_once(callback_delete_message, config.TIMER_TO_DELETE_MESSAGE,
45 | context=(update.message.chat_id, message_id))
46 | context.job_queue.run_once(callback_delete_message, config.TIMER_TO_DELETE_MESSAGE,
47 | context=(update.message.chat_id, update.message.message_id))
48 | return
49 | return func(update, context, *args, **kwargs)
50 | return wrapped
51 |
52 |
53 | def restricted_private_and_group(func):
54 | @wraps(func)
55 | def wrapped(update, context, *args, **kwargs):
56 | if not update.effective_user:
57 | return
58 | user_id = update.effective_user.id
59 | chat_id = update.effective_chat.id
60 | ban_list = context.bot_data.get('ban', [])
61 | if user_id in ban_list or (chat_id < 0 or chat_id not in config.GROUP_IDS):
62 | logger.info('Unauthorized access denied for private and group messages{} {}.'
63 | .format(update.effective_user.full_name, user_id))
64 | return
65 | return func(update, context, *args, **kwargs)
66 | return wrapped
67 |
68 |
69 | def restricted_group_only(func):
70 | @wraps(func)
71 | def wrapped(update, context, *args, **kwargs):
72 | if not update.effective_user:
73 | return
74 | user_id = update.effective_user.id
75 | chat_id = update.effective_chat.id
76 | ban_list = context.bot_data.get('ban', [])
77 | if user_id not in config.USER_IDS and (user_id in ban_list or chat_id > 0 or chat_id not in config.GROUP_IDS):
78 | logger.info('Unauthorized access denied for group only messages {} {}.'
79 | .format(update.effective_user.full_name, user_id))
80 | return
81 | return func(update, context, *args, **kwargs)
82 | return wrapped
83 |
84 |
85 | def restricted_group_and_its_members_in_private(func):
86 | @wraps(func)
87 | def wrapped(update, context, *args, **kwargs):
88 | if not update.effective_user:
89 | return
90 | user_id = update.effective_user.id
91 | chat_id = update.effective_chat.id
92 | ban_list = context.bot_data.get('ban', [])
93 | allow = False
94 | if user_id in config.USER_IDS:
95 | allow = True
96 | elif user_id not in ban_list:
97 | if chat_id < 0:
98 | if chat_id in config.GROUP_IDS:
99 | allow = True
100 | else:
101 | for group_id in config.GROUP_IDS:
102 | info = context.bot.get_chat_member(chat_id=group_id, user_id=update.effective_user.id)
103 | if info.status in ['creator', 'administrator', 'member']:
104 | allow = True
105 | break
106 | if allow is False:
107 | logger.info('Unauthorized access denied for group and its members messages{} {}.'
108 | .format(update.effective_user.full_name, user_id))
109 | return
110 | return func(update, context, *args, **kwargs)
111 | return wrapped
112 |
113 |
114 | def restricted_user_ids(func):
115 | @wraps(func)
116 | def wrapped(update, context, *args, **kwargs):
117 | if not update.effective_user:
118 | return
119 | user_id = update.effective_user.id
120 | if user_id not in config.USER_IDS:
121 | logger.info('Unauthorized access denied for {} {}.'
122 | .format(update.effective_user.full_name, user_id))
123 | return
124 | return func(update, context, *args, **kwargs)
125 | return wrapped
126 |
127 |
128 | def restricted_admin(func):
129 | @wraps(func)
130 | def wrapped(update, context, *args, **kwargs):
131 | if not update.effective_user:
132 | return
133 | user_id = update.effective_user.id
134 | chat_id = update.effective_chat.id
135 | if user_id != config.USER_IDS[0]:
136 | logger.info("Unauthorized admin access denied for {} {}.".format(update.effective_user.full_name, user_id))
137 | return
138 | if chat_id < 0:
139 | return
140 | return func(update, context, *args, **kwargs)
141 | return wrapped
142 |
--------------------------------------------------------------------------------