Compare commits
166 Commits
fc21983305
...
v0.2
Author | SHA1 | Date | |
---|---|---|---|
82467518da | |||
c0085b8000 | |||
a7e79eb254
|
|||
dc774262f8
|
|||
987f642578
|
|||
f7df4d8ddc
|
|||
c2619a1370 | |||
15b272ae35
|
|||
28b5449f2a
|
|||
bfec702bef
|
|||
fd0c4c0545
|
|||
11dbf3239d
|
|||
3d87f035e7
|
|||
d8245934e2
|
|||
420a4cb7eb
|
|||
b747dde664
|
|||
10c60ae932
|
|||
6f8b560acc
|
|||
93f3439a11
|
|||
9e0a815062
|
|||
51da210817
|
|||
e06cb4b377
|
|||
97b3aa1505
|
|||
5adb004a2a | |||
f003638128 | |||
e9e68cb6b3 | |||
bd62149a2c | |||
e5b2584d4c | |||
76fc4981cc | |||
337a7b28aa | |||
93804345df | |||
f5b3335af0 | |||
fc8f49b487 | |||
c6cba6de2f | |||
adef3b3afc | |||
853c3c7cea | |||
fe60b3f8a5 | |||
aa82a8f382 | |||
826e031a39 | |||
0681338970 | |||
7c8e07bbc9 | |||
ad653146e1 | |||
e5f80d9702 | |||
6f03d5c18f | |||
2b520760d3 | |||
9d340b32c1 | |||
4dd754055e | |||
ae654d686e | |||
af2bcd088f | |||
|
cc6523f604 | ||
|
5ad52aa3f8 | ||
|
45789ad013 | ||
|
4422a13ba9 | ||
f82c7b309f | |||
399fc5050d | |||
56adb16a3e | |||
1684c5177c | |||
fba52bcfc4 | |||
20666fc0f8 | |||
03b6bbe039 | |||
1749d49a52 | |||
a4323981fb | |||
06b6b49f43 | |||
1f345e87f7 | |||
717586a9f0 | |||
95351f247c | |||
b5e3abd4ad | |||
4ec69c2a05 | |||
|
62e0a4986c | ||
|
be8f0262d0 | ||
|
ba5a0f116c | ||
|
e85adeafd0 | ||
87d2a2c6d3 | |||
bc6080e832 | |||
9bf4663b9e | |||
b74ce9ec89 | |||
6ed3673682 | |||
c593be156d | |||
c96f98560c | |||
e1c1b58ffb | |||
bd2a1f0b12 | |||
8cc7808afb | |||
6684c4d750 | |||
c670db72fa | |||
48acab2d5e | |||
62f076148c | |||
59b504e6d9 | |||
ad281ba981 | |||
0073120bf2 | |||
244173e556 | |||
98e9c5f5a2 | |||
a8545dd097 | |||
3f22340852 | |||
c2fb88ed65 | |||
88692ebc85 | |||
4331af415e | |||
a913ba19c6 | |||
2387823151 | |||
88f8bb4a52 | |||
cf6c1f03d7 | |||
0a309a9f59 | |||
7810f3b7b9 | |||
7b2534012d | |||
6e8b47cf4b | |||
c27b1c5a5b | |||
c7228a006b | |||
64ae3fb047 | |||
|
056fc52353 | ||
|
8bafd0cb35 | ||
|
fd47217bad | ||
|
6b7b5c22f2 | ||
|
b3698cfa70 | ||
|
7607003f55 | ||
|
7918049f49 | ||
|
fcd59b7aca | ||
|
8c478072c6 | ||
|
b766d0c52c | ||
|
807e629ae7 | ||
|
87af9fd333 | ||
|
a54081a2ae | ||
|
664284a6f8 | ||
|
bd9917fb17 | ||
|
68ea087963 | ||
|
d1813856d9 | ||
|
cf204577e4 | ||
|
68c887999e | ||
|
642e17ee55 | ||
|
25af9b31f8 | ||
|
28fc359593 | ||
|
07203a9db9 | ||
|
663a7b0db8 | ||
|
0d2e9fa6ec | ||
|
c90e5eb697 | ||
|
f4359aa6cd | ||
4cd37be5dc | |||
05042f01c6 | |||
5b1afe2c9e | |||
6eaa6019a3 | |||
493be818b8 | |||
cf09017985 | |||
7940cbe5a7 | |||
8e0fee4cb9 | |||
08e03ba911 | |||
b589da5d8f | |||
a3a1be241c | |||
9ce05589b0 | |||
c1ee988106 | |||
aebe804b58 | |||
9f0ed202da | |||
b4a9595eb4 | |||
569fffd223 | |||
f719befe40 | |||
c4323f0b00 | |||
6facf428c5 | |||
9261585f5f | |||
cddf1e211a | |||
cd5b73e7ff | |||
46ea9a2f74 | |||
7a7f386891 | |||
|
11255bbbd0 | ||
|
8ae50a4f97 | ||
|
46e3665749 | ||
|
0e14ca600f | ||
|
c4177d1575 | ||
|
f0c333968b | ||
|
dfc1e36f07 |
21
.gitignore
vendored
21
.gitignore
vendored
@@ -152,18 +152,13 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
# ---> VisualStudioCode
|
# Custom
|
||||||
.vscode/*
|
config.json
|
||||||
!.vscode/settings.json
|
*.session
|
||||||
!.vscode/tasks.json
|
*.session-journal
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
!.vscode/*.code-snippets
|
|
||||||
|
|
||||||
# Local History for Visual Studio Code
|
venv
|
||||||
.history/
|
venv_linux
|
||||||
|
venv_windows
|
||||||
|
|
||||||
# Built Visual Studio Code Extensions
|
.vscode
|
||||||
*.vsix
|
|
||||||
|
|
||||||
# Project
|
|
20
.renovaterc
Normal file
20
.renovaterc
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:base"
|
||||||
|
],
|
||||||
|
"baseBranches": [
|
||||||
|
"dev"
|
||||||
|
],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"minor",
|
||||||
|
"patch",
|
||||||
|
"pin",
|
||||||
|
"digest"
|
||||||
|
],
|
||||||
|
"automerge": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
112
README.md
112
README.md
@@ -1,60 +1,110 @@
|
|||||||
# TelegramPoster
|
<h1 align="center">TelegramPoster</h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://git.end-play.xyz/profitroll/TelegramPoster/src/branch/master/LICENSE"><img alt="License: GPL" src="https://img.shields.io/badge/License-GPL-blue"></a>
|
||||||
|
<a href="https://git.end-play.xyz/profitroll/TelegramPoster"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
> Шукаєш інструкцію українською? А вона [ось тут](https://git.end-play.xyz/profitroll/TelegramPoster/src/branch/master/README_uk.md) знаходиться)
|
> Шукаєш інструкцію українською? А вона [ось тут](https://git.end-play.xyz/profitroll/TelegramPoster/src/branch/master/README_uk.md) знаходиться)
|
||||||
|
|
||||||
This bot is used for one and only task - post pictures from my personal archive. Here's its source code so you can also host a bot and have fun with it. Just don't exepect it to be brilliant. It is not. But hey, you can always fork it ;)
|
This bot is used for one and only task - post pictures from my personal archive. Here's its source code so you can also host a bot and have fun with it. Just don't exepect it to be brilliant. It is not. But hey, you can always fork it ;)
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
* [Python 3.8+](https://www.python.org) (3.9+ recommended)
|
||||||
|
* [MongoDB](https://www.mongodb.com)
|
||||||
|
* [PhotosAPI](https://git.end-play.xyz/profitroll/PhotosAPI)
|
||||||
|
|
||||||
|
Use [MongoDB's installation manual](https://www.mongodb.com/docs/manual/installation) and [Photos API's README](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md).
|
||||||
|
|
||||||
|
Please note that Photos API also requires MongoDB so it makes sense to install and configure Mongo first.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
To make this bot run at first you need to have a Python interpreter and git. Google is your friend finding it. You can also ignore git and simply download source code, should also work fine. After that you're ready to go.
|
|
||||||
|
To make this bot run at first you need to have a Python interpreter, Photos API, MongoDB and optionally git (if you want to update using `git pull`). You can also ignore git and simply download source code, should also work fine. After that you're ready to go.
|
||||||
|
|
||||||
> In this README I assume that you're using default python in your
|
> In this README I assume that you're using default python in your
|
||||||
> system and your system's PATH contains it. If your default python
|
> system and your system's PATH contains it. If your default python
|
||||||
> is `python3` or for example `/home/user/.local/bin/python3.9` - use it instead.
|
> is `python3` or for example `/home/user/.local/bin/python3.9` - use it instead.
|
||||||
> If it's non-standart executable path - you should also change
|
> If it's non-standard executable path - you should also change
|
||||||
> it in scripts you will use (`loop.sh`, `loop.bat`, `start.sh` and `start.bat`).
|
> it in scripts you will use (`loop.sh`, `loop.bat`, `start.sh` and `start.bat`).
|
||||||
|
|
||||||
1. Download the bot:
|
1. Install MongoDB and Photos API:
|
||||||
1. `git clone https://git.end-play.xyz/profitroll/TelegramSender.git` (if you want to use git)
|
|
||||||
2. `cd ./TelegramSender`
|
1. Install MongoDB by following [official installation manual](https://www.mongodb.com/docs/manual/installation)
|
||||||
|
2. Install Photos API by following [Photos API's README](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md)
|
||||||
|
|
||||||
|
2. Download the bot:
|
||||||
|
|
||||||
|
1. `git clone -b dev https://git.end-play.xyz/profitroll/TelegramPoster.git` (if you're using git)
|
||||||
|
2. `cd TelegramPoster`
|
||||||
|
|
||||||
|
3. Create virtual environment [Optional]:
|
||||||
|
|
||||||
|
1. Install virtualenv module: `pip install virtualenv`
|
||||||
|
2. Create venv: `python -m venv .venv`
|
||||||
|
3. Activate it using `source .venv/bin/activate` on Linux, `.venv\Scripts\activate.bat` in CMD or `.venv\Scripts\Activate.ps1` in PowerShell.
|
||||||
|
|
||||||
|
4. Install project's dependencies:
|
||||||
|
|
||||||
2. Install dependencies:
|
|
||||||
`python -m pip install -r requirements.txt`
|
`python -m pip install -r requirements.txt`
|
||||||
Without installing those - bot cannot work at all
|
Without installing those - bot cannot work at all.
|
||||||
|
|
||||||
3. Install optional dependencies [Not required]:
|
5. Configure required keys with your favorite text editor:
|
||||||
`python -m pip install -r requirements-optional.txt`
|
|
||||||
These are not required but can make the bot run a bit faster
|
|
||||||
|
|
||||||
4. Configure your bot with a favorite text editor:
|
1. Copy config file: `cp config_example.json config.json`
|
||||||
`nano config.json`
|
2. Open `config.json` using your favorite text editor. For example `nano config.json`, but you can also edit it with vim, mcedit, or Notepad/Notepad++ on Windows
|
||||||
You can edit with vim, nano, on Windows it's Notepad or Notepad++. Whatever.
|
3. Change `"bot.owner"`, `"bot.api_id"`, `"bot.api_hash"` and `"bot.bot_token"` keys' values.
|
||||||
If you don't know where to find bot_token and your id - here you can find some hints: [get bot token](https://www.siteguarding.com/en/how-to-get-telegram-bot-api-token), [get your id](https://www.alphr.com/telegram-find-user-id/), [get api_hash and api_id](https://core.telegram.org/api/obtaining_api_id).
|
|
||||||
Also don't forget to change bot's working mode. Dict key `"mode"` contains keys `"post"` and `"submit"`, each of those can be either `true` or `false`.
|
|
||||||
|
|
||||||
5. Add bot to the channel:
|
If you don't know where to find bot_token and your id - here you can find some hints: [get bot token](https://www.siteguarding.com/en/how-to-get-telegram-bot-api-token), [get your id](https://www.alphr.com/telegram-find-user-id), [get api_hash and api_id](https://core.telegram.org/api/obtaining_api_id).
|
||||||
To use your bot of course you need to have a channel or group otherwise makes no sense to have such a bot. [Here](https://stackoverflow.com/a/33497769) you can find a quick guide how to add your bot to a channel.
|
|
||||||
|
|
||||||
6. Fill your contents folder:
|
6. Configure database and API:
|
||||||
Of course bot cannot post something from nothing. Configure your `config.json` what media types bot should post (`"posting", "extensions"`), when to post them (`"posting", "time"`) and also where to find them (`"locations"`). You can also move them when sent by setting `"posting", "move_sent"` to `true`.
|
|
||||||
|
|
||||||
6. Good to go, run it!
|
1. Configure database:
|
||||||
`python ./main.py`
|
1. Change database host and port in keys `"database.host"` and `"database.port"`. For default local installation those will be `127.0.0.1` and `27017` respectively
|
||||||
|
2. Change database name to the one you like in `"database.name"`. It will be automatically created on start
|
||||||
|
3. If you've changed user and password to access the db, you should also change `"database.user"` and `"database.password"` keys, otherwise leave them `null` (default).
|
||||||
|
|
||||||
|
2. Configure Photos API:
|
||||||
|
1. Change `"posting.api.address"` and `"posting.api.address_external"` to the ones your API server uses
|
||||||
|
2. Run your bot using `python main.py --create-user --create-album` to configure its new user and album. You can also use manual user and album creation described [in the wiki](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Configuring-API). You can also change username, password and album in`"posting.api"` to the user and album you have if you already have Photos API album and user set up. In that case you don't need to create a new one.
|
||||||
|
|
||||||
|
7. Add bot to the channel:
|
||||||
|
|
||||||
|
To use your bot of course you need to have a channel or group otherwise it makes no sense to have such a bot. [Here](https://stackoverflow.com/a/33497769) you can find a quick guide how to add your bot to a channel. After that simply set `"posting.channel"` to your channel's ID and `"posting.comments"` to comments group's ID.
|
||||||
|
|
||||||
|
8. Configure posting time:
|
||||||
|
|
||||||
|
To make your bot post random content you need to configure `"posting.time"` with a list of "DD:MM" formatted strings or use `"posting.interval"` formatted as "XdXhXmXs". To use interval instead of selected time, set `"posting.use_interval"` to `true`.
|
||||||
|
|
||||||
|
9. Good to go, run it!
|
||||||
|
|
||||||
|
Make sure MongoDB and Photos API are running and use `python main.py` to start the bot.
|
||||||
Or you can also use `.\start.bat` on Windows and `bash ./start.sh` on Linux.
|
Or you can also use `.\start.bat` on Windows and `bash ./start.sh` on Linux.
|
||||||
Additionally there are `loop.sh` and `loop.bat` available if you want your bot to start again after being stopped or after using `/reboot` command.
|
Additionally there are `loop.sh` and `loop.bat` available if you want your bot to start again after being stopped or after using `/shutdown` command.
|
||||||
|
|
||||||
|
If you need any further instructions on how to configure your bot or you had any difficulties doing so - please use [wiki in this repository](https://git.end-play.xyz/profitroll/TelegramPoster/wiki) to get more detailed instructions.
|
||||||
|
|
||||||
|
## CLI arguments
|
||||||
|
|
||||||
## Command line arguments
|
|
||||||
Of course bot also has them. You can perform some actions with them.
|
Of course bot also has them. You can perform some actions with them.
|
||||||
* `--move-sent` - allows you to move all sent files from queue to sent directories
|
|
||||||
* `--cleanup` - purge files in both `queue` and `sent` folders if they're sent. Requires `--confirm` argument
|
* `--create-user` - create new API user. Requires config key `"posting.api.address"` to be set;
|
||||||
* `--cleanup-index` - purge all sent entries from index. Requires `--confirm` argument
|
* `--create-album` - create new API album. Requires API address and user config (`"posting.api"`) to be complete.
|
||||||
* `--norun` - allows you to execute above arguments without triggering the bot start itself
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
* `python3 ./main.py --move-sent --norun`
|
|
||||||
* `python3 ./main.py --cleanup --confirm`
|
* `python main.py --create-user`
|
||||||
|
* `python main.py --create-user --create-album`
|
||||||
|
|
||||||
|
## Tips and improvements
|
||||||
|
|
||||||
|
* You may want to configure your bot to work as a systemd service instead. There's [a tutorial for that](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Configuring-Service) in the wiki.
|
||||||
|
|
||||||
## Localization
|
## Localization
|
||||||
|
|
||||||
Bot is capable of using custom locales. There are some that are pre-installed (English and Ukrainian), however you can add your own locales too.
|
Bot is capable of using custom locales. There are some that are pre-installed (English and Ukrainian), however you can add your own locales too.
|
||||||
|
|
||||||
All localization files are located in the `locale` folder, otherwise in folder specified in config file. Just copy locale file of your choice, name it in accordance to [IETF language tags](https://en.wikipedia.org/wiki/IETF_language_tag) (if you want your locale to be compatible with Telegram's locales) or define your own name. Save it as json and you're good to go. If you want to change default locale for console output and messages, that cannot determine admin's locale - edit `"locale"` parameter in the `config.json`. If this locale is not available - `"locale_fallback"` will be used instead. If both are not available - error will be shown.
|
All localization files are located in the `locale`. Just copy locale file of your choice, name it in accordance to [IETF language tags](https://en.wikipedia.org/wiki/IETF_language_tag) (if you want your locale to be compatible with Telegram's locales) or define your own name. Save it as json and you're good to go. If you want to change default locale for messages - edit `"locale"` parameter in the `config.json`.
|
||||||
|
|
||||||
We recommend to only make changes to your custom locale. Or at least always have your backup of for example `en.json` as your fallback.
|
We recommend to only make changes to your custom locale. Or at least always have your backup of for example `en.json` as your fallback.
|
134
README_uk.md
134
README_uk.md
@@ -1,58 +1,108 @@
|
|||||||
# TelegramPoster
|
<h1 align="center">TelegramPoster</h1>
|
||||||
Цей бот використовується для однієї-єдиної задачі - розміщувати фотографії з мого особистого архіву. Ось його код, тож Ви також можете захостити бота самостійно та розважитися з ним. Тільки не очікуйте, що він ідеальним. Не буде. Але гей, Ви завжди можете його доробити під себе ;)
|
|
||||||
|
|
||||||
## Установка
|
<p align="center">
|
||||||
Для запуску цього бота спочатку потрібно мати інтерпретатор Python та встановлений git. Google — Ваш друг у пошуках. Ви також можете ігнорувати git і просто завантажити код, також має спрацювати добре. Після цього Ви готові до встановлення.
|
<a href="https://git.end-play.xyz/profitroll/TelegramPoster/src/branch/master/LICENSE"><img alt="License: GPL" src="https://img.shields.io/badge/License-GPL-blue"></a>
|
||||||
|
<a href="https://git.end-play.xyz/profitroll/TelegramPoster"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
> У цьому README я вважаю, що Ви використовуєте python за замовчуванням у своїй
|
Цей бот використовується для однієї-єдиної задачі - публікувати фотографії з мого особистого архіву. Ось його код, тож ви також можете запустити бота і погратися з ним самостійно. Тільки не очікуйте, що він буде ідеальним. Це не так. Але ви завжди можете його форкнути ;)
|
||||||
> системі і PATH Вашої системи містить його. Якщо Ваш python за замовчуванням
|
|
||||||
> є `python3` або, наприклад, `/home/user/.local/bin/python3.9` - використовуйте його натомість.
|
|
||||||
> Якщо це нестандартний шлях до виконуваного файлу - Вам також слід змінити
|
|
||||||
> це у скриптах, які Ви використовуватимете (`loop.sh`, `loop.bat`, `start.sh` та `start.bat`).
|
|
||||||
|
|
||||||
1. Завантажте бота:
|
## Залежності
|
||||||
1. `git clone https://git.end-play.xyz/profitroll/TelegramSender.git` (якщо хочете використовувати git)
|
|
||||||
2. `cd ./TelegramSender`
|
|
||||||
|
|
||||||
2. Встановіть залежності:
|
* [Python 3.8+](https://www.python.org) (рекомендується 3.9+)
|
||||||
`python -m pip install -r requirements.txt`
|
* [MongoDB](https://www.mongodb.com)
|
||||||
Без їх установки бот не зможе працювати взагалі
|
* [PhotosAPI](https://git.end-play.xyz/profitroll/PhotosAPI)
|
||||||
|
|
||||||
3. Встановіть додаткові залежності [Не обов'язково]:
|
Користуйтесь [інструкцією зі встановлення MongoDB](https://www.mongodb.com/docs/manual/installation) та [README Photos API](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md).
|
||||||
`python -m pip install -r requirements-optional.txt`
|
|
||||||
Вони не є обов’язковими, але можуть прискорити роботу бота
|
|
||||||
|
|
||||||
4. Налаштуйте свого бота за допомогою текстового редактора:
|
Зверніть увагу, що Photos API також потребує MongoDB, тому має сенс спочатку встановити й налаштувати Mongo.
|
||||||
`nano config.json`
|
|
||||||
Ви можете редагувати за допомогою vim, nano, у Windows це Notepad або Notepad++. На Ваш смак.
|
|
||||||
Якщо Ви не знаєте, де знайти bot_token і свій ідентифікатор, тут Ви можете знайти кілька підказок: [отримати токен бота](https://www.siteguarding.com/en/how-to-get-telegram-bot-api-token), [отримати свій ідентифікатор](https://www.alphr.com/telegram-find-user-id/), [отримати api_hash і api_id](https://core.telegram.org/api/obtaining_api_id).
|
|
||||||
Також не забудьте змінити режим роботи бота. Ключ `"mode"` має в собі ключі `"post"` та `"submit"`, кожен з який може бути `true` або `false`.
|
|
||||||
|
|
||||||
5. Додайте бота на канал:
|
## Встановлення
|
||||||
Звичайно, щоб використовувати свого бота, Вам потрібно мати канал або групу, інакше немає сенсу мати такого бота. [Тут](https://stackoverflow.com/a/33497769) Ви можете знайти короткий гайд, як додати свого бота до каналу.
|
|
||||||
|
|
||||||
6. Заповніть папку вмістом:
|
Щоб запустити бота, вам потрібно мати інтерпретатор Python, Photos API, MongoDB і, за бажанням, git (якщо ви хочете оновлювати за допомогою `git pull`). Ви також можете проігнорувати git і просто завантажити вихідний код, це також повинно спрацювати. Після цього ви готові до роботи.
|
||||||
Звичайно, бот не може опублікувати щось із нічого. Налаштуйте свій `config.json`, які медіа-типи бот повинен публікувати (`"posting", "extensions"`), коли їх публікувати (`"posting", "time"`), а також де їх знайти (`"locations"`). Ви також можете переміщати їх після надсилання, встановивши для `"posting", "move_sent"` значення `true`.
|
|
||||||
|
|
||||||
6. Готово, запускайте!
|
> У цьому README я припускаю, що ви використовуєте python за замовчуванням у вашій
|
||||||
`python ./main.py`
|
> системі, і він міститься у вашому системному PATH. Якщо ваш python за замовчуванням
|
||||||
Або ви також можете використовувати `.\start.bat` на Windows і `bash ./start.sh` на Linux.
|
> це `python3` або, наприклад, `/home/user/.local/bin/python3.9` - використовуйте його.
|
||||||
Крім того, доступні `loop.sh` і `loop.bat`, якщо ви хочете, щоб ваш бот запускався знову після зупинки або після використання команди `/reboot`.
|
> Якщо це нестандартний шлях до виконуваного файлу - вам також слід змінити
|
||||||
|
> його у скриптах, які ви будете використовувати (`loop.sh`, `loop.bat`, `start.sh` та `start.bat`).
|
||||||
|
|
||||||
## Аргументи командного рядка
|
1. Встановіть MongoDB та Photos API:
|
||||||
Звичайно, у бота вони також є. З ними можна виконувати деякі дії.
|
|
||||||
* `--move-sent` - дозволяє перемістити всі надіслані файли з черги до папки надісланих
|
1. Встановіть MongoDB, дотримуючись [офіційного посібника зі встановлення](https://www.mongodb.com/docs/manual/installation)
|
||||||
* `--cleanup` - очистити файли в папках `queue` і `sent`, якщо вони вже надіслані. Потрібен аргумент `--confirm`
|
2. Встановіть Photos API, дотримуючись [README Photos API](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md)
|
||||||
* `--cleanup-index` - видалити всі надіслані записи з індексу. Потрібен аргумент `--confirm`
|
|
||||||
* `--norun` - дозволяє виконувати наведені вище аргументи, не запускаючи самого бота
|
2. Завантажте бота:
|
||||||
|
|
||||||
|
1. `git clone https://git.end-play.xyz/profitroll/TelegramPoster.git` (якщо ви використовуєте git)
|
||||||
|
2. `cd TelegramPoster`
|
||||||
|
|
||||||
|
3. Створіть віртуальне середовище [Необов'язково]:
|
||||||
|
|
||||||
|
1. Встановіть модуль virtualenv: `pip install virtualenv`
|
||||||
|
2. Створіть venv: `python -m venv .venv`
|
||||||
|
3. Активуйте його за допомогою `ource .venv/bin/activate` в Linux, `.venv\Scripts\activate.bat` в CMD або `.venv\Scripts\Activate.ps1` в PowerShell.
|
||||||
|
|
||||||
|
4. Встановіть залежності проекту:
|
||||||
|
|
||||||
|
`python -m pip install -r requirements.txt`.
|
||||||
|
Без їх встановлення бот не зможе працювати взагалі.
|
||||||
|
|
||||||
|
5. Налаштуйте необхідні ключі за допомогою вашого улюбленого текстового редактора:
|
||||||
|
|
||||||
|
1. Скопіюйте конфігураційний файл: `cp config_example.json config.json`
|
||||||
|
2. Відкрийте `config.json` за допомогою вашого улюбленого текстового редактора. Наприклад, `nano config.json`, але ви також можете відредагувати його за допомогою vim, mcedit або Notepad/Notepad++ на Windows
|
||||||
|
3. Змініть значення ключів `"bot.owner"`, `"bot.api_id"`, `"bot.api_hash"` і `"bot.bot_token"`.
|
||||||
|
|
||||||
|
Якщо ви не знаєте, де знайти bot_token і ваш id - тут ви можете знайти кілька підказок: [отримати токен бота](https://www.siteguarding.com/en/how-to-get-telegram-bot-api-token), [отримати свій id](https://www.alphr.com/telegram-find-user-id), [отримати api_hash та api_id](https://core.telegram.org/api/obtaining_api_id).
|
||||||
|
|
||||||
|
6. Налаштування бази даних та API:
|
||||||
|
|
||||||
|
1. Налаштуйте базу даних:
|
||||||
|
1. Змініть хост і порт бази даних у ключах `"database.host"` і `"database.port"`. Для локальної установки за замовчуванням це будуть `127.0.0.1` і `27017` відповідно
|
||||||
|
2. Змініть ім'я бази даних в `"database.name"`. Вона буде автоматично створена при запуску
|
||||||
|
3. Якщо ви змінили користувача та пароль для доступу до бази даних, вам також слід змінити ключі `"database.user"` та `"database.password"`, інакше залиште їх `null` (за замовчуванням).
|
||||||
|
|
||||||
|
2. Налаштуйте Photos API:
|
||||||
|
1. Змініть `"posting.api.address"` та `"posting.api.address_external"` на ті, що використовує ваш сервер API
|
||||||
|
2. Запустіть бота за допомогою `python main.py --create-user --create-album`, щоб налаштувати нового користувача та альбом. Ви також можете скористатися ручним створенням користувача і альбому, описаним [у вікі](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Configuring-API). Ви також можете змінити ім'я користувача, пароль і альбом у `"posting.api"` на користувача і альбом, які у вас є, якщо у вас вже налаштовані альбом і користувач Photos API. У цьому випадку вам не потрібно створювати нові.
|
||||||
|
|
||||||
|
7. Додайте бота до каналу:
|
||||||
|
|
||||||
|
Щоб використовувати бота, вам, звичайно, потрібно мати канал або групу, інакше немає сенсу мати такого бота. [Тут](https://stackoverflow.com/a/33497769) ви можете знайти короткий посібник, як додати бота до каналу. Після цього просто встановіть `"posting.channel"` на ID вашого каналу і `"posting.comments"` на ID групи коментарів.
|
||||||
|
|
||||||
|
8. Налаштуйте час публікації:
|
||||||
|
|
||||||
|
Щоб ваш бот публікував випадковий контент, вам потрібно налаштувати `"posting.time"` зі списком рядків у форматі "ДД:ММ" або використовувати `"posting.interval"` у форматі "XdXhXmXs". Щоб використовувати інтервал замість вибраного часу, встановіть `"posting.use_interval"` у значення `true`.
|
||||||
|
|
||||||
|
9. Готово, запускайте!
|
||||||
|
|
||||||
|
Переконайтеся, що MongoDB і Photos API запущені і використовуйте `python main.py` для запуску бота.
|
||||||
|
Або ви також можете використовувати `.\start.bat` в Windows і `bash ./start.sh` в Linux.
|
||||||
|
Додатково доступні `loop.sh` і `loop.bat`, якщо ви хочете, щоб ваш бот запустився знову після зупинки або після використання команди `/shutdown`.
|
||||||
|
|
||||||
|
Якщо вам потрібні додаткові інструкції щодо налаштування бота або у вас виникли труднощі - скористайтеся [вікі в цьому репозиторії](https://git.end-play.xyz/profitroll/TelegramPoster/wiki), щоб отримати детальніші інструкції.
|
||||||
|
|
||||||
|
## CLI аргументи
|
||||||
|
|
||||||
|
Звичайно, бот також має CLI аргументи. За допомогою них можна виконувати деякі дії.
|
||||||
|
|
||||||
|
* `--create-user` - створити нового користувача API. Потребує встановленого конфігураційного ключа `"posting.api.address"`;
|
||||||
|
* `--create-album` - створити новий альбом API. Вимагає заповнених адреси API та конфігурації користувача (`"posting.api"`).
|
||||||
|
|
||||||
Приклади:
|
Приклади:
|
||||||
* `python3 ./main.py --move-sent --norun`
|
|
||||||
* `python3 ./main.py --cleanup --confirm`
|
* `python main.py --create-user`
|
||||||
|
* `python main.py --create-user --create-album`
|
||||||
|
|
||||||
|
## Поради та покращення
|
||||||
|
|
||||||
|
* Можливо, ви захочете налаштувати бота для роботи як системну службу. У вікі є [сторінка з цього питання](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Configuring-Service).
|
||||||
|
|
||||||
## Локалізація
|
## Локалізація
|
||||||
Бот може використовувати різні мови. Є деякі попередньо встановлені (Англійська та Українська), однак Ви можете додавати свої власні локалізації теж.
|
|
||||||
|
|
||||||
Всі файли локалізації знаходяться у папці `locale`, якщо в конфігураційному файлі не вказано іншу. Просто скопіюйте цікавлячий Вас файл, назвіть його відповідно до [тегів мови IETF](https://en.wikipedia.org/wiki/IETF_language_tag) (якщо Ви хочете, щоб переклад був сумісним з перекладами Telegram) або просто вкажіть свою власну назву. Збережіть свій переклад як json файл і все готово. Якщо ви хочете змінити мову за замовчуванням для виведення консолі та повідомлень самого бота, які не можуть визначити мову адміністратора, відредагуйте параметр `"locale"` у `config.json`. Якщо ця мова недоступна, замість неї буде використано `"locale_fallback"`. Якщо обидві мови недоступні - буде показано помилку.
|
Бот може використовувати файли локалізації. Деякі з них встановлено за замовчуванням (англійська та українська), але ви також можете додавати свої власні.
|
||||||
|
|
||||||
Ми рекомендуємо вносити будь-які зміни лише до вашої окремої мови. Або, принаймні, завжди мати резервну копію, наприклад, `en.json` як запасний варіант.
|
Усі файли локалізації знаходяться у теці `locale`. Просто скопіюйте файл локалі за вашим вибором, назвіть його відповідно до [мовних кодів IETF](https://en.wikipedia.org/wiki/IETF_language_tag) (якщо ви хочете, щоб ваша локаль була сумісна з локалями Telegram) або дайте йому власну назву. Збережіть переклад у форматі json, і все буде готово. Якщо ви хочете змінити локаль за замовчуванням для повідомлень - відредагуйте параметр `"locale"` у файлі `config.json`.
|
||||||
|
|
||||||
|
Ми рекомендуємо вносити зміни лише у вашу власну локаль. Або, принаймні, завжди мати резервну копію, наприклад, `en.json` як запасний варіант.
|
||||||
|
8
classes/enums/submission_types.py
Normal file
8
classes/enums/submission_types.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionType(Enum):
|
||||||
|
DOCUMENT = "document"
|
||||||
|
VIDEO = "video"
|
||||||
|
# ANIMATION = "animation"
|
||||||
|
PHOTO = "photo"
|
63
classes/exceptions.py
Normal file
63
classes/exceptions.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionUnavailableError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionUploadError(Exception):
|
||||||
|
def __init__(self, file_path: str, status_code: int, content: Any) -> None:
|
||||||
|
self.status_code = status_code
|
||||||
|
self.content = content
|
||||||
|
super().__init__(
|
||||||
|
f"Could not upload photo '{file_path}' due to HTTP {self.status_code}: {self.content}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionDuplicatesError(Exception):
|
||||||
|
def __init__(self, file_path: str, duplicates: list) -> None:
|
||||||
|
self.duplicates = duplicates
|
||||||
|
super().__init__(
|
||||||
|
f"Found duplicates of a photo '{file_path}': {self.duplicates}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionUnsupportedError(Exception):
|
||||||
|
def __init__(self, file_path: str) -> None:
|
||||||
|
super().__init__(f"Type of file does not seem to be supported: '{file_path}'")
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreationError(Exception):
|
||||||
|
def __init__(self, code: int, data: str) -> None:
|
||||||
|
self.code = code
|
||||||
|
self.data = data
|
||||||
|
super().__init__(
|
||||||
|
f"Could not create a new user. API returned HTTP {self.code} with content: {self.data}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreationDuplicateError(Exception):
|
||||||
|
def __init__(self, username: str) -> None:
|
||||||
|
self.username = username
|
||||||
|
super().__init__(f"User '{self.username} already exists.'")
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumCreationError(Exception):
|
||||||
|
def __init__(self, code: int, data: str) -> None:
|
||||||
|
self.code = code
|
||||||
|
self.data = data
|
||||||
|
super().__init__(
|
||||||
|
f"Could not create a new album. API returned HTTP {self.code} with content: {self.data}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumCreationDuplicateError(Exception):
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
self.name = name
|
||||||
|
super().__init__(f"Album '{self.name} already exists.'")
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumCreationNameError(Exception):
|
||||||
|
def __init__(self, data: dict) -> None:
|
||||||
|
self.data = data
|
||||||
|
super().__init__(data["detail"])
|
280
classes/pyroclient.py
Normal file
280
classes/pyroclient.py
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from io import BytesIO
|
||||||
|
from os import makedirs, remove, sep
|
||||||
|
from pathlib import Path
|
||||||
|
from shutil import rmtree
|
||||||
|
from time import time
|
||||||
|
from traceback import format_exc
|
||||||
|
from typing import Dict, List, Tuple, Union
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
from bson import ObjectId
|
||||||
|
from libbot import json_write
|
||||||
|
from libbot.i18n.sync import _
|
||||||
|
from libbot.pyrogram.classes import PyroClient
|
||||||
|
from photosapi_client.errors import UnexpectedStatus
|
||||||
|
from pyrogram.errors import bad_request_400
|
||||||
|
from pyrogram.types import Message, User
|
||||||
|
from pytimeparse.timeparse import timeparse
|
||||||
|
from ujson import dumps, loads
|
||||||
|
|
||||||
|
from classes.enums.submission_types import SubmissionType
|
||||||
|
from classes.exceptions import (
|
||||||
|
SubmissionDuplicatesError,
|
||||||
|
SubmissionUnavailableError,
|
||||||
|
SubmissionUnsupportedError,
|
||||||
|
)
|
||||||
|
from classes.pyrouser import PyroUser
|
||||||
|
from modules.api_client import (
|
||||||
|
BodyPhotoUpload,
|
||||||
|
BodyVideoUpload,
|
||||||
|
File,
|
||||||
|
Photo,
|
||||||
|
Video,
|
||||||
|
client,
|
||||||
|
photo_upload,
|
||||||
|
video_upload,
|
||||||
|
)
|
||||||
|
from modules.database import col_submitted, col_users
|
||||||
|
from modules.http_client import http_session
|
||||||
|
from modules.sender import send_content
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PyroClient(PyroClient):
|
||||||
|
def __init__(self, scheduler: AsyncIOScheduler):
|
||||||
|
super().__init__(locales_root=Path("locale"), scheduler=scheduler)
|
||||||
|
|
||||||
|
self.version: float = 0.2
|
||||||
|
|
||||||
|
self.owner: int = self.config["bot"]["owner"]
|
||||||
|
self.admins: List[int] = self.config["bot"]["admins"] + [
|
||||||
|
self.config["bot"]["owner"]
|
||||||
|
]
|
||||||
|
|
||||||
|
self.sender_session = ClientSession()
|
||||||
|
|
||||||
|
self.scopes_placeholders: Dict[str, int] = {
|
||||||
|
"owner": self.owner,
|
||||||
|
"comments": self.config["posting"]["comments"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
await super().start()
|
||||||
|
|
||||||
|
if self.config["reports"]["update"]:
|
||||||
|
try:
|
||||||
|
async with ClientSession(
|
||||||
|
json_serialize=dumps,
|
||||||
|
) as http_session:
|
||||||
|
check_update = await http_session.get(
|
||||||
|
"https://git.end-play.xyz/api/v1/repos/profitroll/TelegramPoster/releases?page=1&limit=1"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await check_update.json()
|
||||||
|
|
||||||
|
if len(response) == 0:
|
||||||
|
raise ValueError("No bot releases on git found.")
|
||||||
|
|
||||||
|
if float(response[0]["tag_name"].replace("v", "")) > self.version:
|
||||||
|
logger.info(
|
||||||
|
"New version %s found (current %s)",
|
||||||
|
response[0]["tag_name"].replace("v", ""),
|
||||||
|
self.version,
|
||||||
|
)
|
||||||
|
await self.send_message(
|
||||||
|
self.owner,
|
||||||
|
self._(
|
||||||
|
"update_available",
|
||||||
|
"message",
|
||||||
|
).format(
|
||||||
|
response[0]["tag_name"],
|
||||||
|
response[0]["html_url"],
|
||||||
|
response[0]["body"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info("No updates found, bot is up to date.")
|
||||||
|
except bad_request_400.PeerIdInvalid:
|
||||||
|
logger.warning(
|
||||||
|
"Could not send startup message to bot owner. Perhaps user has not started the bot yet."
|
||||||
|
)
|
||||||
|
except Exception as exp:
|
||||||
|
logger.exception("Update check failed due to %s: %s", exp, format_exc())
|
||||||
|
|
||||||
|
if self.config["mode"]["post"]:
|
||||||
|
if self.config["posting"]["use_interval"]:
|
||||||
|
self.scheduler.add_job(
|
||||||
|
send_content,
|
||||||
|
"interval",
|
||||||
|
seconds=timeparse(self.config["posting"]["interval"]),
|
||||||
|
args=[self, self.sender_session],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for entry in self.config["posting"]["time"]:
|
||||||
|
dt_obj = datetime.strptime(entry, "%H:%M")
|
||||||
|
self.scheduler.add_job(
|
||||||
|
send_content,
|
||||||
|
"cron",
|
||||||
|
hour=dt_obj.hour,
|
||||||
|
minute=dt_obj.minute,
|
||||||
|
args=[self, self.sender_session],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
makedirs(self.config["locations"]["cache"], exist_ok=True)
|
||||||
|
await json_write(
|
||||||
|
{"timestamp": time()},
|
||||||
|
Path(f"{self.config['locations']['cache']}/shutdown_time"),
|
||||||
|
)
|
||||||
|
|
||||||
|
await http_session.close()
|
||||||
|
await self.sender_session.close()
|
||||||
|
|
||||||
|
await super().stop()
|
||||||
|
|
||||||
|
async def submit_media(
|
||||||
|
self, id: str
|
||||||
|
) -> Tuple[Union[Message, None], Union[str, None]]:
|
||||||
|
db_entry = col_submitted.find_one({"_id": ObjectId(id)})
|
||||||
|
submission = None
|
||||||
|
|
||||||
|
if db_entry is None:
|
||||||
|
raise SubmissionUnavailableError()
|
||||||
|
|
||||||
|
if db_entry["temp"]["uuid"] is None:
|
||||||
|
try:
|
||||||
|
submission = await self.get_messages(
|
||||||
|
db_entry["user"], db_entry["telegram"]["msg_id"]
|
||||||
|
)
|
||||||
|
filepath = await self.download_media(
|
||||||
|
submission, file_name=self.config["locations"]["tmp"] + sep
|
||||||
|
)
|
||||||
|
except Exception as exp:
|
||||||
|
raise SubmissionUnavailableError() from exp
|
||||||
|
|
||||||
|
elif not Path(
|
||||||
|
f"{self.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}/{db_entry['temp']['file']}",
|
||||||
|
).exists():
|
||||||
|
raise SubmissionUnavailableError()
|
||||||
|
else:
|
||||||
|
filepath = Path(
|
||||||
|
f"{self.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}/{db_entry['temp']['file']}",
|
||||||
|
)
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
submission = await self.get_messages(
|
||||||
|
db_entry["user"], db_entry["telegram"]["msg_id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
async with aiofiles.open(str(filepath), "rb") as fh:
|
||||||
|
media_bytes = BytesIO(await fh.read())
|
||||||
|
|
||||||
|
try:
|
||||||
|
if db_entry["type"] == SubmissionType.PHOTO.value:
|
||||||
|
response = await photo_upload(
|
||||||
|
self.config["posting"]["api"]["album"],
|
||||||
|
client=client,
|
||||||
|
multipart_data=BodyPhotoUpload(
|
||||||
|
File(media_bytes, filepath.name, "image/jpeg")
|
||||||
|
),
|
||||||
|
ignore_duplicates=self.config["submission"]["allow_duplicates"],
|
||||||
|
compress=False,
|
||||||
|
caption="queue",
|
||||||
|
)
|
||||||
|
elif db_entry["type"] == SubmissionType.VIDEO.value:
|
||||||
|
response = await video_upload(
|
||||||
|
self.config["posting"]["api"]["album"],
|
||||||
|
client=client,
|
||||||
|
multipart_data=BodyVideoUpload(
|
||||||
|
File(media_bytes, filepath.name, "video/*")
|
||||||
|
),
|
||||||
|
caption="queue",
|
||||||
|
)
|
||||||
|
# elif db_entry["type"] == SubmissionType.ANIMATION.value:
|
||||||
|
# response = await video_upload(
|
||||||
|
# self.config["posting"]["api"]["album"],
|
||||||
|
# client=client,
|
||||||
|
# multipart_data=BodyVideoUpload(
|
||||||
|
# File(media_bytes, filepath.name, "video/*")
|
||||||
|
# ),
|
||||||
|
# caption="queue",
|
||||||
|
# )
|
||||||
|
except UnexpectedStatus as exp:
|
||||||
|
raise SubmissionUnsupportedError(str(filepath)) from exp
|
||||||
|
|
||||||
|
response_dict = (
|
||||||
|
{}
|
||||||
|
if not hasattr(response, "content")
|
||||||
|
else loads(response.content.decode("utf-8"))
|
||||||
|
)
|
||||||
|
|
||||||
|
if "duplicates" in response_dict and len(response_dict["duplicates"]) > 0:
|
||||||
|
duplicates = []
|
||||||
|
for index, duplicate in enumerate(response_dict["duplicates"]): # type: ignore
|
||||||
|
if response_dict["access_token"] is None:
|
||||||
|
duplicates.append(
|
||||||
|
f"`{duplicate['id']}`:\n{self.config['posting']['api']['address_external']}/photos/{duplicate['id']}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
duplicates.append(
|
||||||
|
f"`{duplicate['id']}`:\n{self.config['posting']['api']['address_external']}/token/photo/{response_dict['access_token']}?id={index}"
|
||||||
|
)
|
||||||
|
raise SubmissionDuplicatesError(str(filepath), duplicates)
|
||||||
|
|
||||||
|
col_submitted.find_one_and_update(
|
||||||
|
{"_id": ObjectId(id)}, {"$set": {"done": True}}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if db_entry["temp"]["uuid"] is not None:
|
||||||
|
rmtree(
|
||||||
|
Path(
|
||||||
|
f"{self.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}",
|
||||||
|
),
|
||||||
|
ignore_errors=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
remove(str(filepath))
|
||||||
|
except (FileNotFoundError, NotADirectoryError):
|
||||||
|
logger.error("Could not delete '%s' on submission accepted", filepath)
|
||||||
|
|
||||||
|
return (
|
||||||
|
submission,
|
||||||
|
response.parsed.id if hasattr(response, "parsed") else response.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def find_user(self, user: Union[int, User]) -> PyroUser:
|
||||||
|
"""Find User by it's ID or User object
|
||||||
|
|
||||||
|
### Args:
|
||||||
|
* user (`Union[int, User]`): ID or User object to extract ID from
|
||||||
|
|
||||||
|
### Returns:
|
||||||
|
* `PyroUser`: PyroUser object
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
col_users.find_one(
|
||||||
|
{"id": user.id if isinstance(user, User) else user}
|
||||||
|
) # type: ignore
|
||||||
|
is None
|
||||||
|
):
|
||||||
|
col_users.insert_one(
|
||||||
|
{
|
||||||
|
"id": user.id if isinstance(user, User) else user,
|
||||||
|
"locale": user.language_code if isinstance(user, User) else None,
|
||||||
|
"banned": False,
|
||||||
|
"cooldown": datetime(1970, 1, 1, 0, 0),
|
||||||
|
"subscription": {"expires": datetime(1970, 1, 1, 0, 0)},
|
||||||
|
}
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
db_record = col_users.find_one(
|
||||||
|
{"id": user.id if isinstance(user, User) else user}
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
return PyroUser(**db_record)
|
55
classes/pyrouser.py
Normal file
55
classes/pyrouser.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from bson import ObjectId
|
||||||
|
from libbot import config_get
|
||||||
|
from libbot.pyrogram.classes import PyroClient
|
||||||
|
|
||||||
|
from modules.database import col_users
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PyroUser:
|
||||||
|
"""Dataclass of DB entry of a user"""
|
||||||
|
|
||||||
|
_id: ObjectId
|
||||||
|
id: int
|
||||||
|
locale: Union[str, None]
|
||||||
|
banned: bool
|
||||||
|
cooldown: datetime
|
||||||
|
subscription: dict
|
||||||
|
|
||||||
|
async def update_locale(self, locale: str):
|
||||||
|
col_users.update_one({"_id": self._id}, {"$set": {"locale": locale}})
|
||||||
|
|
||||||
|
async def update_cooldown(self, time: datetime = datetime.now()):
|
||||||
|
col_users.update_one({"_id": self._id}, {"$set": {"cooldown": time}})
|
||||||
|
|
||||||
|
async def block(self) -> None:
|
||||||
|
"""Ban user from using command and submitting content."""
|
||||||
|
col_users.update_one({"_id": self._id}, {"$set": {"banned": True}})
|
||||||
|
|
||||||
|
async def unblock(self) -> None:
|
||||||
|
"""Allow user to use command and submit posts again."""
|
||||||
|
col_users.update_one({"_id": self._id}, {"$set": {"banned": False}})
|
||||||
|
|
||||||
|
async def is_limited(self, app: Union[PyroClient, None] = None) -> bool:
|
||||||
|
"""Check if user is on a cooldown after submitting something.
|
||||||
|
|
||||||
|
### Returns:
|
||||||
|
`bool`: Must be `True` if on the cooldown and `False` if not
|
||||||
|
"""
|
||||||
|
admins = (
|
||||||
|
app.admins
|
||||||
|
if app is not None
|
||||||
|
else (
|
||||||
|
await config_get("admins", "bot") + [await config_get("owner", "bot")]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return (datetime.now() - self.cooldown).total_seconds() < (
|
||||||
|
app.config["submission"]["timeout"]
|
||||||
|
if app is not None
|
||||||
|
else await config_get("timeout", "submission")
|
||||||
|
)
|
87
config.json
87
config.json
@@ -1,87 +0,0 @@
|
|||||||
{
|
|
||||||
"module": null,
|
|
||||||
"locale": "en",
|
|
||||||
"locale_fallback": "en",
|
|
||||||
"admin": 0,
|
|
||||||
"bot": {
|
|
||||||
"api_id": 0,
|
|
||||||
"api_hash": "",
|
|
||||||
"bot_token": ""
|
|
||||||
},
|
|
||||||
"mode": {
|
|
||||||
"post": true,
|
|
||||||
"submit": true
|
|
||||||
},
|
|
||||||
"reports": {
|
|
||||||
"sent": false,
|
|
||||||
"error": true,
|
|
||||||
"startup": true,
|
|
||||||
"shutdown": true
|
|
||||||
},
|
|
||||||
"logging": {
|
|
||||||
"size": 512,
|
|
||||||
"location": "logs"
|
|
||||||
},
|
|
||||||
"locations": {
|
|
||||||
"data": "data",
|
|
||||||
"sent": "data/sent",
|
|
||||||
"queue": "data/queue",
|
|
||||||
"index": "data/index.json",
|
|
||||||
"submit": "data/submit.json",
|
|
||||||
"blocked": "data/blocked.json",
|
|
||||||
"locale": "locale"
|
|
||||||
},
|
|
||||||
"posting": {
|
|
||||||
"channel": 0,
|
|
||||||
"silent": false,
|
|
||||||
"move_sent": false,
|
|
||||||
"extensions": {
|
|
||||||
"photo": [
|
|
||||||
"jpg",
|
|
||||||
"png",
|
|
||||||
"gif",
|
|
||||||
"jpeg"
|
|
||||||
],
|
|
||||||
"video": [
|
|
||||||
"mp4",
|
|
||||||
"avi",
|
|
||||||
"mkv",
|
|
||||||
"webm",
|
|
||||||
"mov"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"time": [
|
|
||||||
"08:00",
|
|
||||||
"10:00",
|
|
||||||
"12:00",
|
|
||||||
"14:00",
|
|
||||||
"16:00",
|
|
||||||
"18:00",
|
|
||||||
"20:00",
|
|
||||||
"22:00"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"caption": {
|
|
||||||
"enabled": false,
|
|
||||||
"text": "sample text",
|
|
||||||
"link": null
|
|
||||||
},
|
|
||||||
"submission": {
|
|
||||||
"timeout": 30,
|
|
||||||
"file_size": 15728640,
|
|
||||||
"mime_types": [
|
|
||||||
"image/png",
|
|
||||||
"image/gif",
|
|
||||||
"image/jpeg",
|
|
||||||
"video/mp4",
|
|
||||||
"video/quicktime"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"commands": [
|
|
||||||
"start",
|
|
||||||
"rules"
|
|
||||||
],
|
|
||||||
"commands_admin": [
|
|
||||||
"reboot"
|
|
||||||
]
|
|
||||||
}
|
|
204
config_example.json
Normal file
204
config_example.json
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
{
|
||||||
|
"locale": "en",
|
||||||
|
"bot": {
|
||||||
|
"owner": 0,
|
||||||
|
"admins": [],
|
||||||
|
"api_id": 0,
|
||||||
|
"api_hash": "",
|
||||||
|
"bot_token": "",
|
||||||
|
"max_concurrent_transmissions": 3,
|
||||||
|
"scoped_commands": true
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"user": null,
|
||||||
|
"password": null,
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 27017,
|
||||||
|
"name": "tgposter"
|
||||||
|
},
|
||||||
|
"reports": {
|
||||||
|
"chat_id": "owner",
|
||||||
|
"sent": false,
|
||||||
|
"error": true,
|
||||||
|
"update": true,
|
||||||
|
"startup": true,
|
||||||
|
"shutdown": true
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"post": true,
|
||||||
|
"submit": true
|
||||||
|
},
|
||||||
|
"logging": {
|
||||||
|
"size": 512,
|
||||||
|
"location": "logs"
|
||||||
|
},
|
||||||
|
"locations": {
|
||||||
|
"tmp": "tmp",
|
||||||
|
"data": "data",
|
||||||
|
"cache": "cache"
|
||||||
|
},
|
||||||
|
"disabled_plugins": [],
|
||||||
|
"posting": {
|
||||||
|
"channel": 0,
|
||||||
|
"comments": 0,
|
||||||
|
"silent": false,
|
||||||
|
"move_sent": false,
|
||||||
|
"use_interval": false,
|
||||||
|
"interval": "1h30m",
|
||||||
|
"submitted_caption": {
|
||||||
|
"enabled": true,
|
||||||
|
"ignore_admins": true,
|
||||||
|
"text": "#submitted"
|
||||||
|
},
|
||||||
|
"types": {
|
||||||
|
"photo": true,
|
||||||
|
"video": false
|
||||||
|
},
|
||||||
|
"extensions": {
|
||||||
|
"photo": [
|
||||||
|
"jpg",
|
||||||
|
"png",
|
||||||
|
"jpeg"
|
||||||
|
],
|
||||||
|
"video": [
|
||||||
|
"mp4",
|
||||||
|
"avi",
|
||||||
|
"mkv",
|
||||||
|
"webm",
|
||||||
|
"mov"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": [
|
||||||
|
"08:00",
|
||||||
|
"10:00",
|
||||||
|
"12:00",
|
||||||
|
"14:00",
|
||||||
|
"16:00",
|
||||||
|
"18:00",
|
||||||
|
"20:00",
|
||||||
|
"22:00"
|
||||||
|
],
|
||||||
|
"api": {
|
||||||
|
"address": "http://localhost:8054",
|
||||||
|
"address_external": "https://photos.domain.com",
|
||||||
|
"username": "",
|
||||||
|
"password": "",
|
||||||
|
"album": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"caption": {
|
||||||
|
"enabled": false,
|
||||||
|
"link": null,
|
||||||
|
"text": [
|
||||||
|
"sample text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"submission": {
|
||||||
|
"timeout": 30,
|
||||||
|
"file_size": 15728640,
|
||||||
|
"tmp_size": 15728640,
|
||||||
|
"allow_duplicates": false,
|
||||||
|
"send_uploaded_id": false,
|
||||||
|
"require_confirmation": {
|
||||||
|
"users": true,
|
||||||
|
"admins": true
|
||||||
|
},
|
||||||
|
"mime_types": [
|
||||||
|
"image/png",
|
||||||
|
"image/jpeg",
|
||||||
|
"video/mp4",
|
||||||
|
"video/quicktime"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"start": {
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "BotCommandScopeDefault"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BotCommandScopeChat",
|
||||||
|
"chat_id": "owner"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "BotCommandScopeDefault"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BotCommandScopeChat",
|
||||||
|
"chat_id": "owner"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "BotCommandScopeDefault"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BotCommandScopeChat",
|
||||||
|
"chat_id": "owner"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"report": {
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "BotCommandScopeChat",
|
||||||
|
"chat_id": "comments"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"forwards": {
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "BotCommandScopeChat",
|
||||||
|
"chat_id": "owner"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "BotCommandScopeChat",
|
||||||
|
"chat_id": "owner"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "BotCommandScopeChat",
|
||||||
|
"chat_id": "owner"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"remove": {
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "BotCommandScopeChat",
|
||||||
|
"chat_id": "owner"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"purge": {
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "BotCommandScopeChat",
|
||||||
|
"chat_id": "owner"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"shutdown": {
|
||||||
|
"scopes": [
|
||||||
|
{
|
||||||
|
"name": "BotCommandScopeChat",
|
||||||
|
"chat_id": "owner"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1 +0,0 @@
|
|||||||
[]
|
|
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"last_id": 0,
|
|
||||||
"sent": [],
|
|
||||||
"captions": {}
|
|
||||||
}
|
|
@@ -1 +0,0 @@
|
|||||||
{}
|
|
1
docs/config_api.md
Normal file
1
docs/config_api.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Configuring API connection
|
1
docs/config_channel.md
Normal file
1
docs/config_channel.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Configuring channel and adding your bot
|
1
docs/config_posts.md
Normal file
1
docs/config_posts.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Configuring posts and duplicates checker
|
1
docs/config_systemd.md
Normal file
1
docs/config_systemd.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Configuring as systemd service
|
1
docs/config_time.md
Normal file
1
docs/config_time.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Configuring posting time
|
1
docs/updating.md
Normal file
1
docs/updating.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Updating your bot
|
@@ -1,28 +1,81 @@
|
|||||||
{
|
{
|
||||||
|
"metadata": {
|
||||||
|
"flag": "🇬🇧",
|
||||||
|
"name": "English",
|
||||||
|
"codes": [
|
||||||
|
"en",
|
||||||
|
"en-US",
|
||||||
|
"en-GB"
|
||||||
|
]
|
||||||
|
},
|
||||||
"commands": {
|
"commands": {
|
||||||
"start": "Start using the bot",
|
"start": "Start using the bot",
|
||||||
"rules": "Photos submission rules"
|
"rules": "Photos submission rules",
|
||||||
},
|
"language": "Change bot's language",
|
||||||
"commands_admin": {
|
"report": "Report this post",
|
||||||
"forwards": "Check post forwards",
|
"forwards": "Check post forwards",
|
||||||
"reboot": "Restart the bot"
|
"import": "Submit .zip archive with photos",
|
||||||
|
"export": "Get .zip archive with all photos",
|
||||||
|
"remove": "Delete photo by its ID",
|
||||||
|
"purge": "Completely purge bot's queue",
|
||||||
|
"shutdown": "Turn off the bot"
|
||||||
},
|
},
|
||||||
"message": {
|
"message": {
|
||||||
"start": "Hi and welcome!\n\nYou can submit your pictures and videos here. We'll review and add them, if we like them. Make sure you send your stuff one at a time and have chosen media that corresponds to our rules.\n\nYou can also write something to us in the description field. We'll send it with the submission itself, if needed.\n\nAlso, make sure you follow the /rules of submission, otherwise your submission will be declined. In case of spam/abuse you may even be blocked.\n\nHave fun and happy submitting!",
|
"start": "Hi and welcome!\n\nYou can submit your pictures and videos here. We'll review and add them, if we like them. Make sure you send your stuff one at a time and have chosen media that corresponds to our rules.\n\nYou can also write something to us in the description field. We'll send it with the submission itself, if needed.\n\nAlso, make sure you follow the /rules of submission, otherwise your submission will be declined. In case of spam/abuse you may even be blocked.\n\nHave fun and happy submitting!",
|
||||||
"rules": "Photos submission rules:\n1. No porn, only erotics and aesthetics\n2. Nipples are semi-allowed, should be either veiled or barely visible\n3. Genitalia strictly prohibited, but labia prints on clothes or nice pubes/panties/butts - are fine",
|
"rules": "Photos submission rules:\n1. No porn, only erotics and aesthetics\n2. Nipples are semi-allowed, should be either veiled or barely visible\n3. Genitalia strictly prohibited, but labia prints on clothes or nice pubes/panties/butts - are fine\n4. Submitting russians is forbidden",
|
||||||
"shutdown": "Shutting down bot with pid `{0}`",
|
"shutdown": "Shutting down bot with pid `{0}`",
|
||||||
"startup": "Starting with pid `{0}`",
|
"startup": "Starting with pid `{0}`",
|
||||||
|
"startup_downtime_minutes": "Starting with pid `{0}` (was down for {1} m.)",
|
||||||
|
"startup_downtime_hours": "Starting with pid `{0}` (was down for {1} h.)",
|
||||||
|
"startup_downtime_days": "Starting with pid `{0}` (was down for {1} d.)",
|
||||||
"sub_yes": "✅ Submission approved and accepted",
|
"sub_yes": "✅ Submission approved and accepted",
|
||||||
|
"sub_yes_auto": "✅ Submission automatically accepted",
|
||||||
"sub_no": "❌ Submission reviewed and declined",
|
"sub_no": "❌ Submission reviewed and declined",
|
||||||
|
"sub_dup": "⚠️ Submission automatically declined because database already contains this photo",
|
||||||
"sub_blocked": "You were blocked and you can't submit media anymore.",
|
"sub_blocked": "You were blocked and you can't submit media anymore.",
|
||||||
"sub_unblocked": "You were unblocked and you can now submit media.",
|
"sub_unblocked": "You were unblocked and you can now submit media.",
|
||||||
"sub_by": "\n\nSubmitted by:",
|
"sub_by": "\n\nSubmitted by:",
|
||||||
"sub_sent": "Media has been submitted.\nWe'll notify you whether it will be accepted or not soon.",
|
"sub_sent": "Media has been submitted.\nWe'll notify you whether it will be accepted or not soon.",
|
||||||
"sub_cooldown": "You can only submit 1 media per {0} seconds",
|
"sub_cooldown": "You can only submit 1 media per {0} seconds",
|
||||||
|
"sub_media_failed": "Could not upload submission to API. Please check logs for details.",
|
||||||
|
"sub_media_duplicates": "⚠️ Image duplicates found",
|
||||||
|
"sub_media_duplicates_list": "It seems like following image has duplicates in API's Db.\n\nNext files marked as similar:\n • {0}",
|
||||||
"document_too_large": "File you've sent is too large. Please submit files not bigger than {0} MB",
|
"document_too_large": "File you've sent is too large. Please submit files not bigger than {0} MB",
|
||||||
"mime_not_allowed": "File type not allowed. Please, consider using one of these: {0}",
|
"mime_not_allowed": "File type not allowed. Please, consider using one of these: {0}",
|
||||||
"post_exception": "Could not send content due to `{exp}`\n\nTraceback:\n```{0}```",
|
"post_exception": "Could not send content due to `{0}`\n\nTraceback:\n```{1}```",
|
||||||
"post_empty": "Could not send content: `Queue folder is empty or contains only unsupported or already sent files.`"
|
"post_invalid_pic": "⚠️ Error {0} while sending photo\n```python\n{1}\n```",
|
||||||
|
"api_queue_empty": "Could not send content: `Queue is empty or contains only unsupported files.`",
|
||||||
|
"api_queue_error": "Could not get photo from API's queue. Check the log above or API's errors to get more info.",
|
||||||
|
"post_low": "Low amount of content: `There are only {0} files left in the queue.`",
|
||||||
|
"api_creds_invalid": "Could not authorize API access. Please check whether provided in config file are valid and update them if they're not.",
|
||||||
|
"sub_wip": "Post submission is now WIP. It will be available again in a few days. Thank you for your patience.",
|
||||||
|
"sub_error": "⚠️ Could not upload this image due to bot error. Admins are advised.",
|
||||||
|
"sub_error_admin": "User {0} could not submit photo without additional confirmation due to:\n```\n{1}\n```",
|
||||||
|
"import_request": "Please send me a zip archive with your media to be imported. Use /cancel if you want to abort this operation.",
|
||||||
|
"import_ignored": "No response, aborting import.",
|
||||||
|
"import_abort": "Import aborted.",
|
||||||
|
"import_invalid_media": "File to import must be a zip archive. Aborting.",
|
||||||
|
"import_invalid_mime": "Provided file is not supported. Please send `application/zip`. Aborting.",
|
||||||
|
"import_too_big": "You archive is `{0} GiB` big, but system has only `{1} GiB` free. Unpacking may take even more space. Aborting.",
|
||||||
|
"import_downloading": "Downloading archive...",
|
||||||
|
"import_unpacking": "Unpacking archive...",
|
||||||
|
"import_unpack_error": "Could not unpack the archive\n\nException: {0}\n\nTraceback:\n```python\n{1}\n```",
|
||||||
|
"import_uploading": "Uploading archive contents...",
|
||||||
|
"import_upload_error_duplicate": "Could not upload `{0}` because there're duplicates on server.",
|
||||||
|
"import_upload_error_other": "Could not upload `{0}`. Probably disallowed filetype.",
|
||||||
|
"import_finished": "Import finished.",
|
||||||
|
"locale_choice": "Alright. Please choose the language using keyboard below.",
|
||||||
|
"remove_request": "Please send me an ID to delete. You might have it from upload dialog. Use /cancel if you want to abort this operation.",
|
||||||
|
"remove_ignored": "No response, aborting removal.",
|
||||||
|
"remove_abort": "Removal aborted.",
|
||||||
|
"remove_success": "Removed media with ID `{0}`.",
|
||||||
|
"remove_failure": "Could not remove media with ID `{0}`. Check if provided ID is correct and if it is - you can also check bot's log for details.",
|
||||||
|
"remove_kind": "Please choose the type of media to delete. Use /cancel if you want to abort this operation.",
|
||||||
|
"remove_unknown": "Unknown media type. It can only be \"{0}\" or \"{1}\".",
|
||||||
|
"update_available": "**New version found**\nThere's a newer version of a bot found. You can update your bot to [{0}]({1}) using command line of your host.\n\n**Release notes**\n{2}\n\nRead more about updating you bot on the [wiki page](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Updating-Instance).\n\nPlease not that you can also disable this notification by editing `reports.update` key of the config.",
|
||||||
|
"shutdown_confirm": "There are {0} unfinished users' contexts. If you turn off the bot, those will be lost. Please confirm shutdown using a button below.",
|
||||||
|
"report_sent": "We've notified admins about presumable violation. Thank you for cooperation.",
|
||||||
|
"report_received": "This message has been reported by **{0}** (@{1}, `{2}`)"
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"sub_yes": "✅ Accept",
|
"sub_yes": "✅ Accept",
|
||||||
@@ -30,7 +83,12 @@
|
|||||||
"sub_no": "❌ Deny",
|
"sub_no": "❌ Deny",
|
||||||
"sub_block": "☠️ Block sender",
|
"sub_block": "☠️ Block sender",
|
||||||
"sub_unblock": "🏳️ Unblock sender",
|
"sub_unblock": "🏳️ Unblock sender",
|
||||||
"post_view": "View in channel"
|
"post_view": "View in channel",
|
||||||
|
"accepted": "✅ Accepted",
|
||||||
|
"declined": "❌ Declined",
|
||||||
|
"shutdown": "Confirm shutdown",
|
||||||
|
"photo": "Photo",
|
||||||
|
"video": "Video"
|
||||||
},
|
},
|
||||||
"callback": {
|
"callback": {
|
||||||
"sub_yes": "✅ Submission approved",
|
"sub_yes": "✅ Submission approved",
|
||||||
@@ -38,25 +96,10 @@
|
|||||||
"sub_block": "User {0} has been blocked",
|
"sub_block": "User {0} has been blocked",
|
||||||
"sub_unblock": "User {0} has been unblocked",
|
"sub_unblock": "User {0} has been unblocked",
|
||||||
"sub_msg_unavail": "Submission message no longer exist",
|
"sub_msg_unavail": "Submission message no longer exist",
|
||||||
"sub_media_unavail": "Could not download submission"
|
"sub_media_unavail": "Could not download submission",
|
||||||
},
|
"sub_done": "You've already decided what to do with submission",
|
||||||
"console": {
|
"sub_duplicates_found": "There're duplicates in bot's database",
|
||||||
"shutdown": "Shutting down bot with pid {0}",
|
"locale_set": "Your language now is: {locale}",
|
||||||
"startup":"Starting with pid {0}",
|
"nothing": "🏁 This action is already finished"
|
||||||
"keyboard_interrupt": "\nShutting down...",
|
|
||||||
"exception_occured": "Exception {0} happened on task execution",
|
|
||||||
"post_sent": "Sent {0} of type {1} to {2} with caption {3} and silently {4}",
|
|
||||||
"post_exception": "Could not send content due to {0}. Traceback: {1}",
|
|
||||||
"post_empty": "Could not send content due to queue folder empty with allowed extensions",
|
|
||||||
"deps_missing": "Required modules are not installed. Run 'pip3 install -r requirements.txt' and restart the program.",
|
|
||||||
"passed_norun": "Argument --norun passed, not running the main script",
|
|
||||||
"move_sent_doesnt_exist": "File '{0}' is already moved or does not exist",
|
|
||||||
"move_sent_doesnt_exception": "Could not move sent file '{0}' to '{1}' due to {2}",
|
|
||||||
"move_sent_completed": "Moved all sent files to the sent folder",
|
|
||||||
"cleanup_exception": "Could not remove '{0}' due to {1}",
|
|
||||||
"cleanup_completed": "Performed cleanup of the sent files",
|
|
||||||
"cleanup_unathorized": "Requested cleanup of sent files but not authorized. Please pass '--confirm' to perform that",
|
|
||||||
"cleanup_index_completed": "Performed cleanup of sent files index",
|
|
||||||
"cleanup_index_unathorized": "Requested cleanup of sent files index but not authorized. Please pass '--confirm' to perform that"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
102
locale/uk.json
102
locale/uk.json
@@ -1,28 +1,80 @@
|
|||||||
{
|
{
|
||||||
|
"metadata": {
|
||||||
|
"flag": "🇺🇦",
|
||||||
|
"name": "Українська",
|
||||||
|
"codes": [
|
||||||
|
"uk",
|
||||||
|
"uk-UA"
|
||||||
|
]
|
||||||
|
},
|
||||||
"commands": {
|
"commands": {
|
||||||
"start": "Почати користуватись ботом",
|
"start": "Почати користуватись ботом",
|
||||||
"rules": "Правила пропонування фото"
|
"rules": "Правила пропонування фото",
|
||||||
},
|
"language": "Змінити мову бота",
|
||||||
"commands_admin": {
|
"report": "Поскаржитись на цей пост",
|
||||||
"forwards": "Переглянути репости",
|
"forwards": "Переглянути репости",
|
||||||
"reboot": "Перезапустити бота"
|
"import": "Надати боту .zip архів з фотографіями",
|
||||||
|
"export": "Отримати .zip архів з усіма фотографіями",
|
||||||
|
"remove": "Видалити фото за його ID",
|
||||||
|
"purge": "Повністю видалити всю чергу бота",
|
||||||
|
"shutdown": "Вимкнути бота"
|
||||||
},
|
},
|
||||||
"message": {
|
"message": {
|
||||||
"start": "Привіт і ласкаво просимо!\n\nТут можна пропонувати свої фотографії та відео. Ми переглянемо та додамо їх, якщо вони нам сподобаються. Переконайтеся, що ви надсилаєте свої матеріали по одному та вибираєте медіа, які відповідають нашим правилам.\n\nВи також можете написати нам щось у полі опису. За потреби ми надішлемо це разом із самим фото.\n\nКрім того, переконайтеся, що ви дотримуєтеся /rules (правил) подання, інакше вашу пропозицію буде відхилено. У разі спаму/зловживань вас можуть навіть заблокувати.\n\nГарного дня та щасливого надсилання!",
|
"start": "Привіт і ласкаво просимо!\n\nТут можна пропонувати свої фотографії та відео. Ми переглянемо та додамо їх, якщо вони нам сподобаються. Переконайтеся, що ви надсилаєте свої матеріали по одному та вибираєте медіа, які відповідають нашим правилам.\n\nВи також можете написати нам щось у полі опису. За потреби ми надішлемо це разом із самим фото.\n\nКрім того, переконайтеся, що ви дотримуєтеся /rules (правил) подання, інакше вашу пропозицію буде відхилено. У разі спаму/зловживань вас можуть навіть заблокувати.\n\nГарного дня та щасливого надсилання!",
|
||||||
"rules": "Правила пропонування фото:\n1. Ніякого порно, тільки еротика та естетика\n2. Соски можна, але або завуальовані, або зовсім ледь помітні\n3. Геніталії суворо ні, а ось відбитки статевих губ на одязі або гарні лобочки/трусики/попки - без проблем",
|
"rules": "Правила пропонування фото:\n1. Ніякого порно, тільки еротика та естетика\n2. Соски можна, але або завуальовані, або зовсім ледь помітні\n3. Геніталії суворо ні, а ось відбитки статевих губ на одязі або гарні лобочки/трусики/попки - без проблем\n4. Пропонувати русню заборонено",
|
||||||
"shutdown": "Вимкнення бота з підом `{0}`",
|
"shutdown": "Вимкнення бота з підом `{0}`",
|
||||||
"startup": "Запуск бота з підом `{0}`",
|
"startup": "Запуск бота з підом `{0}`",
|
||||||
|
"startup_downtime_minutes": "Запуск бота з підом `{0}` (лежав {1} хв.)",
|
||||||
|
"startup_downtime_hours": "Запуск бота з підом `{0}` (лежав {1} год.)",
|
||||||
|
"startup_downtime_days": "Запуск бота з підом `{0}` (лежав {1} дн.)",
|
||||||
"sub_yes": "✅ Подання схвалено та прийнято",
|
"sub_yes": "✅ Подання схвалено та прийнято",
|
||||||
|
"sub_yes_auto": "✅ Подання автоматично прийнято",
|
||||||
"sub_no": "❌ Подання розглянуто та відхилено",
|
"sub_no": "❌ Подання розглянуто та відхилено",
|
||||||
"sub_blocked": "Вас заблокували, і ви більше не можете надсилати медіафайли.",
|
"sub_dup": "⚠️ Подання автоматично відхилено через наявність цього фото в базі даних",
|
||||||
"sub_unblocked": "Вас розблокували, і тепер ви можете надсилати медіафайли.",
|
"sub_blocked": "Вас заблокували, ви більше не можете надсилати медіафайли.",
|
||||||
|
"sub_unblocked": "Вас розблокували, тепер ви можете надсилати медіафайли.",
|
||||||
"sub_by": "\n\nПредставлено:",
|
"sub_by": "\n\nПредставлено:",
|
||||||
"sub_sent": "Медіа-файл надіслано.\nСкоро ми повідомимо вас, чи буде його прийнято.",
|
"sub_sent": "Медіа-файл надіслано.\nСкоро ми повідомимо вас, чи буде його прийнято.",
|
||||||
"sub_cooldown": "Ви можете надсилати лише 1 медіафайл на {0} секунд",
|
"sub_cooldown": "Ви можете надсилати лише 1 медіафайл на {0} секунд",
|
||||||
|
"sub_media_failed": "Не вдалося завантажити подання на сервер. Перевірте логи для деталей.",
|
||||||
|
"sub_media_duplicates": "⚠️ Знайдено зображення-дублікати",
|
||||||
|
"sub_media_duplicates_list": "Здається, подане зображення має дублікати в базі даних.\n\nНаступні файли було відмічено як дуже схожі з поданням:\n • {0}",
|
||||||
"document_too_large": "Надісланий файл завеликий. Будь ласка, надсилайте файли не більше {0} Мб",
|
"document_too_large": "Надісланий файл завеликий. Будь ласка, надсилайте файли не більше {0} Мб",
|
||||||
"mime_not_allowed": "Тип файлу не дозволений. Розгляньте можливість використання одного з цих: {0}",
|
"mime_not_allowed": "Тип файлу не дозволений. Розгляньте можливість використання одного з цих: {0}",
|
||||||
"post_exception": "Не вдалося надіслати контент через `{exp}`\n\nTraceback:\n```{0}```",
|
"post_exception": "Не вдалося надіслати контент через `{0}`\n\nTraceback:\n```{1}```",
|
||||||
"post_empty": "Не вдалося надіслати контент: «Папка черги порожня або містить лише непідтримувані або вже надіслані файли»."
|
"post_invalid_pic": "⚠️ Помилка надсилання фото {0}\n```python\n{1}\n```",
|
||||||
|
"api_queue_empty": "Не вдалося надіслати контент: `Черга порожня або містить лише непідтримувані файли`.",
|
||||||
|
"api_queue_error": "Не вдалось отримати фото з черги API. Погляньте на логи вище а також на лог помилок API щоб дізнатись подробиці.",
|
||||||
|
"post_low": "Мала кількість контенту: `Залишилось всього {0} файлів в черзі.`",
|
||||||
|
"api_creds_invalid": "Не вдалося авторизувати запит до API. Будь ласка, перевірте чи дані авторизації в конфігураційному файлі вірні та оновіть їх, якщо це не так.",
|
||||||
|
"sub_wip": "Подання постів зараз знаходиться у розробці. Він буде знову доступний через кілька днів. Дякуємо за ваше терпіння.",
|
||||||
|
"sub_error": "⚠️ Не вдалось завантажити фото через помилку бота. Адміністрацію повідомлено.",
|
||||||
|
"sub_error_admin": "Користувач {0} не зміг надіслати фото без додаткової перевірки через помилку:\n```\n{1}\n```",
|
||||||
|
"import_request": "Будь ласка, надішліть zip-архів з медіа для імпортування. Використовуйте /cancel, якщо ви хочете перервати цю операцію.",
|
||||||
|
"import_ignored": "Немає відповіді, перериваємо імпорт.",
|
||||||
|
"import_abort": "Імпорт перервано.",
|
||||||
|
"import_invalid_media": "Файл для імпорту має бути zip-архівом. Перериваємо.",
|
||||||
|
"import_invalid_mime": "Наданий файл не підтримується. Будь ласка, надішліть `application/zip`. Перервано.",
|
||||||
|
"import_too_big": "Ваш архів має розмір `{0} GiB`, але система має лише `{1} GiB` вільних. Розпакування може зайняти значно більше місця. Перервано.",
|
||||||
|
"import_downloading": "Завантажуємо архів...",
|
||||||
|
"import_unpacking": "Розпаковуємо архів...",
|
||||||
|
"import_unpack_error": "Не вдалося розпакувати архів\n\nПомилка: {0}\n\nTraceback:\n```python\n{1}\n```",
|
||||||
|
"import_uploading": "Завантажуємо вміст архіву...",
|
||||||
|
"import_upload_error_duplicate": "Не вдалося завантажити `{0}`, оскільки на сервері є дублікати.",
|
||||||
|
"import_upload_error_other": "Не вдалося завантажити `{0}`. Ймовірно, заборонений тип файлу.",
|
||||||
|
"import_finished": "Імпорт завершено.",
|
||||||
|
"locale_choice": "Гаразд. Будь ласка, оберіть мову за допомогою клавіатури нижче.",
|
||||||
|
"remove_request": "Будь ласка, надішліть мені ID для видалення. Ви могли отримати його з діалогу завантаження. Використовуйте /cancel, якщо ви хочете перервати цю операцію.",
|
||||||
|
"remove_ignored": "Немає відповіді, перериваємо видалення.",
|
||||||
|
"remove_abort": "Видалення перервано.",
|
||||||
|
"remove_success": "Видалено медіа з ID `{0}`.",
|
||||||
|
"remove_failure": "Не вдалося видалити медіа з ID `{0}`. Перевірте, чи вказано правильний ID, і якщо він правильний, ви також можете переглянути логи бота для отримання більш детальної інформації.",
|
||||||
|
"remove_kind": "Будь ласка, оберіть тип контенту для видалення. Використовуйте /cancel, якщо ви хочете перервати цю операцію.",
|
||||||
|
"remove_unknown": "Невідомий тип контенту. Може бути тільки \"{0}\" або \"{1}\".",
|
||||||
|
"update_available": "**Знайдено нову версію**\nЗнайдено нову версію бота. Ви можете оновити бота до [{0}]({1}) за допомогою командного рядка вашого хосту.\n\n**Примітки до релізу**\n{2}\n\nДетальніше про оновлення бота можна знайти на [вікі-сторінці](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Updating-Instance).\n\nЗверніть увагу, що ви також можете вимкнути це сповіщення, відредагувавши ключ `reports.update` у конфігурації.",
|
||||||
|
"shutdown_confirm": "Існує {0} незавершених контекстів користувачів. Якщо ви вимкнете бота, вони будуть втрачені. Будь ласка, підтвердіть вимкнення за допомогою кнопки нижче.",
|
||||||
|
"report_sent": "Ми повідомили адміністрацію про потенційне порушення. Дякую за співпрацю.",
|
||||||
|
"report_received": "На це повідомлення було отримано скаргу від **{0}** (@{1}, `{2}`)"
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"sub_yes": "✅ Прийняти",
|
"sub_yes": "✅ Прийняти",
|
||||||
@@ -30,7 +82,12 @@
|
|||||||
"sub_no": "❌ Відхилити",
|
"sub_no": "❌ Відхилити",
|
||||||
"sub_block": "☠️ Заблокувати відправника",
|
"sub_block": "☠️ Заблокувати відправника",
|
||||||
"sub_unblock": "🏳️ Розблокувати відправника",
|
"sub_unblock": "🏳️ Розблокувати відправника",
|
||||||
"post_view": "Переглянути на каналі"
|
"post_view": "Переглянути на каналі",
|
||||||
|
"accepted": "✅ Прийнято",
|
||||||
|
"declined": "❌ Відхилено",
|
||||||
|
"shutdown": "Підтвердити вимкнення",
|
||||||
|
"photo": "Фото",
|
||||||
|
"video": "Відео"
|
||||||
},
|
},
|
||||||
"callback": {
|
"callback": {
|
||||||
"sub_yes": "✅ Подання схвалено",
|
"sub_yes": "✅ Подання схвалено",
|
||||||
@@ -38,25 +95,10 @@
|
|||||||
"sub_block": "Користувача {0} заблоковано",
|
"sub_block": "Користувача {0} заблоковано",
|
||||||
"sub_unblock": "Користувача {0} розблоковано",
|
"sub_unblock": "Користувача {0} розблоковано",
|
||||||
"sub_msg_unavail": "Повідомлення більше не існує",
|
"sub_msg_unavail": "Повідомлення більше не існує",
|
||||||
"sub_media_unavail": "Не вдалося завантажити подання"
|
"sub_media_unavail": "Не вдалося завантажити подання",
|
||||||
},
|
"sub_done": "Ви вже обрали що зробити з цим поданням",
|
||||||
"console": {
|
"sub_duplicates_found": "Знайдено дублікати в базі даних бота",
|
||||||
"shutdown": "Вимкнення бота з підом {0}",
|
"locale_set": "Встановлено мову: {locale}",
|
||||||
"startup": "Запуск бота з підом {0}",
|
"nothing": "🏁 Цю дію вже було завершено"
|
||||||
"keyboard_interrupt": "\nВимикаюсь...",
|
|
||||||
"exception_occured": "Помилка {0} сталась під час виконання",
|
|
||||||
"post_sent": "Надіслано {0} типу {1} у {2} з підписом {3} та без звуку {4}",
|
|
||||||
"post_exception": "Не вдалося надіслати контент через {0}. Traceback: {1}",
|
|
||||||
"post_empty": "Не вдалося надіслати контент через порожню папку черги з дозволеними розширеннями",
|
|
||||||
"deps_missing": "Необхідні модулі не встановлені. Запустіть 'pip3 install -r requirements.txt' і перезапустіть програму.",
|
|
||||||
"passed_norun": "Аргумент --norun надано, основний скрипт не запускається",
|
|
||||||
"move_sent_doesnt_exist": "Файл '{0}' уже переміщено або він не існує",
|
|
||||||
"move_sent_doesnt_exception": "Неможливо перемістити надісланий файл '{0}' до '{1}' через {2}",
|
|
||||||
"move_sent_completed": "Переміщено всі надіслані файли до папки надісланих",
|
|
||||||
"cleanup_exception": "Не вдалося видалити '{0}' через {1}",
|
|
||||||
"cleanup_completed": "Виконано очищення надісланих файлів",
|
|
||||||
"cleanup_unathorized": "Надіслано запит на очищення надісланих файлів, але не авторизовано. Для цього надайте аргумент '--confirm'",
|
|
||||||
"cleanup_index_completed": "Виконано очищення індексу надісланих файлів",
|
|
||||||
"cleanup_index_unathorized": "Надіслано запит на очищення індексу надісланих файлів, але не авторизовано. Для цього надайте аргумент '--confirm'"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
2
loop.bat
2
loop.bat
@@ -4,7 +4,7 @@ REM You can cd to your directory here, if you want
|
|||||||
REM cd C:\Users\user\TelegramPoster
|
REM cd C:\Users\user\TelegramPoster
|
||||||
|
|
||||||
:start
|
:start
|
||||||
python poster.py
|
python main.py
|
||||||
echo To completely stop TelegramPoster now, please press Ctrl+C during the countdown!
|
echo To completely stop TelegramPoster now, please press Ctrl+C during the countdown!
|
||||||
echo Restarting in 5 seconds...
|
echo Restarting in 5 seconds...
|
||||||
Timeout /t 5
|
Timeout /t 5
|
||||||
|
2
loop.sh
2
loop.sh
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
while true
|
while true
|
||||||
do
|
do
|
||||||
python poster.py
|
python main.py
|
||||||
echo "To completely stop TelegramPoster now, please press Ctrl+C during the countdown!"
|
echo "To completely stop TelegramPoster now, please press Ctrl+C during the countdown!"
|
||||||
echo "Restarting in:"
|
echo "Restarting in:"
|
||||||
for i in 5 4 3 2 1
|
for i in 5 4 3 2 1
|
||||||
|
43
main.py
Normal file
43
main.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
|
from os import getpid
|
||||||
|
|
||||||
|
from convopyro import Conversation
|
||||||
|
|
||||||
|
# This import MUST be done earlier than PyroClient!
|
||||||
|
# Even if isort does not like it...
|
||||||
|
from modules.cli import *
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules.scheduler import scheduler
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(name)s.%(funcName)s | %(levelname)s | %(message)s",
|
||||||
|
datefmt="[%X]",
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
with contextlib.suppress(ImportError):
|
||||||
|
import uvloop
|
||||||
|
|
||||||
|
uvloop.install()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
client = PyroClient(scheduler=scheduler)
|
||||||
|
Conversation(client)
|
||||||
|
|
||||||
|
try:
|
||||||
|
client.run()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.warning("Forcefully shutting down with PID %s...", getpid())
|
||||||
|
finally:
|
||||||
|
if client.scheduler is not None:
|
||||||
|
client.scheduler.shutdown()
|
||||||
|
exit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
153
modules/api_client.py
Normal file
153
modules/api_client.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from base64 import b64decode, b64encode
|
||||||
|
from os import makedirs, path, sep
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
from libbot import config_get, i18n, sync
|
||||||
|
from photosapi_client import AuthenticatedClient, Client
|
||||||
|
from photosapi_client.api.default.album_create_albums_post import (
|
||||||
|
asyncio as album_create,
|
||||||
|
)
|
||||||
|
from photosapi_client.api.default.album_delete_album_id_delete import (
|
||||||
|
asyncio as album_delete,
|
||||||
|
)
|
||||||
|
from photosapi_client.api.default.album_find_albums_get import asyncio as album_find
|
||||||
|
from photosapi_client.api.default.login_for_access_token_token_post import sync as login
|
||||||
|
from photosapi_client.api.default.photo_delete_photos_id_delete import (
|
||||||
|
asyncio as photo_delete,
|
||||||
|
)
|
||||||
|
from photosapi_client.api.default.photo_find_albums_album_photos_get import (
|
||||||
|
asyncio as photo_find,
|
||||||
|
)
|
||||||
|
from photosapi_client.api.default.photo_get_photos_id_get import asyncio as photo_get
|
||||||
|
from photosapi_client.api.default.photo_patch_photos_id_patch import (
|
||||||
|
asyncio as photo_patch,
|
||||||
|
)
|
||||||
|
from photosapi_client.api.default.photo_random_albums_album_photos_random_get import (
|
||||||
|
asyncio as photo_random,
|
||||||
|
)
|
||||||
|
from photosapi_client.api.default.photo_upload_albums_album_photos_post import (
|
||||||
|
asyncio_detailed as photo_upload,
|
||||||
|
)
|
||||||
|
from photosapi_client.api.default.user_create_users_post import asyncio as user_create
|
||||||
|
from photosapi_client.api.default.user_me_users_me_get import sync as user_me
|
||||||
|
from photosapi_client.api.default.video_delete_videos_id_delete import (
|
||||||
|
asyncio as video_delete,
|
||||||
|
)
|
||||||
|
from photosapi_client.api.default.video_find_albums_album_videos_get import (
|
||||||
|
asyncio as video_find,
|
||||||
|
)
|
||||||
|
from photosapi_client.api.default.video_get_videos_id_get import asyncio as video_get
|
||||||
|
from photosapi_client.api.default.video_patch_videos_id_patch import (
|
||||||
|
asyncio as video_patch,
|
||||||
|
)
|
||||||
|
from photosapi_client.api.default.video_random_albums_album_videos_random_get import (
|
||||||
|
asyncio as video_random,
|
||||||
|
)
|
||||||
|
from photosapi_client.api.default.video_upload_albums_album_videos_post import (
|
||||||
|
asyncio as video_upload,
|
||||||
|
)
|
||||||
|
from photosapi_client.models.body_login_for_access_token_token_post import (
|
||||||
|
BodyLoginForAccessTokenTokenPost,
|
||||||
|
)
|
||||||
|
from photosapi_client.models.body_photo_upload_albums_album_photos_post import (
|
||||||
|
BodyPhotoUploadAlbumsAlbumPhotosPost as BodyPhotoUpload,
|
||||||
|
)
|
||||||
|
from photosapi_client.models.body_video_upload_albums_album_videos_post import (
|
||||||
|
BodyVideoUploadAlbumsAlbumVideosPost as BodyVideoUpload,
|
||||||
|
)
|
||||||
|
from photosapi_client.models.http_validation_error import HTTPValidationError
|
||||||
|
from photosapi_client.models.photo import Photo
|
||||||
|
from photosapi_client.models.photo_search import PhotoSearch
|
||||||
|
from photosapi_client.models.token import Token
|
||||||
|
from photosapi_client.models.video import Video
|
||||||
|
from photosapi_client.models.video_search import VideoSearch
|
||||||
|
from photosapi_client.types import File
|
||||||
|
|
||||||
|
from modules.http_client import http_session
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def authorize(custom_session: Union[ClientSession, None] = None) -> str:
|
||||||
|
makedirs(await config_get("cache", "locations"), exist_ok=True)
|
||||||
|
session = http_session if custom_session is None else custom_session
|
||||||
|
|
||||||
|
if path.exists(await config_get("cache", "locations") + sep + "api_access") is True:
|
||||||
|
async with aiofiles.open(
|
||||||
|
await config_get("cache", "locations") + sep + "api_access", "rb"
|
||||||
|
) as file:
|
||||||
|
token = b64decode(await file.read()).decode("utf-8")
|
||||||
|
if (
|
||||||
|
await session.get(
|
||||||
|
await config_get("address", "posting", "api") + "/users/me/",
|
||||||
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
|
)
|
||||||
|
).status == 200:
|
||||||
|
return token
|
||||||
|
payload = {
|
||||||
|
"grant_type": "password",
|
||||||
|
"scope": "me albums.list albums.read albums.write photos.list photos.read photos.write videos.list videos.read videos.write",
|
||||||
|
"username": await config_get("username", "posting", "api"),
|
||||||
|
"password": await config_get("password", "posting", "api"),
|
||||||
|
}
|
||||||
|
response = await session.post(
|
||||||
|
await config_get("address", "posting", "api") + "/token", data=payload
|
||||||
|
)
|
||||||
|
if not response.ok:
|
||||||
|
logger.warning(
|
||||||
|
"Incorrect API credentials! Could not login into '%s' using login '%s': HTTP %s",
|
||||||
|
await config_get("address", "posting", "api"),
|
||||||
|
await config_get("username", "posting", "api"),
|
||||||
|
response.status,
|
||||||
|
)
|
||||||
|
raise ValueError
|
||||||
|
async with aiofiles.open(
|
||||||
|
str(Path(f"{await config_get('cache', 'locations')}/api_access")),
|
||||||
|
"wb",
|
||||||
|
) as file:
|
||||||
|
await file.write(
|
||||||
|
b64encode((await response.json())["access_token"].encode("utf-8"))
|
||||||
|
)
|
||||||
|
return (await response.json())["access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
unauthorized_client = Client(
|
||||||
|
base_url=sync.config_get("address", "posting", "api"),
|
||||||
|
timeout=5.0,
|
||||||
|
verify_ssl=True,
|
||||||
|
raise_on_unexpected_status=True,
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
login_token = login(
|
||||||
|
client=unauthorized_client,
|
||||||
|
form_data=BodyLoginForAccessTokenTokenPost(
|
||||||
|
grant_type="password",
|
||||||
|
scope="me albums.list albums.read albums.write photos.list photos.read photos.write videos.list videos.read videos.write",
|
||||||
|
username=sync.config_get("username", "posting", "api"),
|
||||||
|
password=sync.config_get("password", "posting", "api"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(login_token, Token):
|
||||||
|
logger.warning(
|
||||||
|
"Could not initialize connection due to invalid token: %s", login_token
|
||||||
|
)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
client = AuthenticatedClient(
|
||||||
|
base_url=sync.config_get("address", "posting", "api"),
|
||||||
|
timeout=5.0,
|
||||||
|
verify_ssl=True,
|
||||||
|
raise_on_unexpected_status=True,
|
||||||
|
token=login_token.access_token,
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(asyncio.run(authorize()))
|
117
modules/cli.py
Normal file
117
modules/cli.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import asyncio
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
from sys import exit
|
||||||
|
from traceback import print_exc
|
||||||
|
|
||||||
|
from libbot import config_get, config_set, sync
|
||||||
|
from photosapi_client.api.default.album_create_albums_post import (
|
||||||
|
asyncio as album_create,
|
||||||
|
)
|
||||||
|
from photosapi_client.api.default.login_for_access_token_token_post import (
|
||||||
|
asyncio as login,
|
||||||
|
)
|
||||||
|
from photosapi_client.api.default.user_create_users_post import asyncio as user_create
|
||||||
|
from photosapi_client.client import AuthenticatedClient, Client
|
||||||
|
from photosapi_client.models.body_login_for_access_token_token_post import (
|
||||||
|
BodyLoginForAccessTokenTokenPost,
|
||||||
|
)
|
||||||
|
from photosapi_client.models.body_user_create_users_post import BodyUserCreateUsersPost
|
||||||
|
|
||||||
|
parser = ArgumentParser(
|
||||||
|
prog="Telegram Poster",
|
||||||
|
description="Bot for posting some of your stuff and also receiving submissions.",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument("--create-user", action="store_true")
|
||||||
|
parser.add_argument("--create-album", action="store_true")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
unauthorized_client = Client(
|
||||||
|
base_url=sync.config_get("address", "posting", "api"),
|
||||||
|
timeout=5.0,
|
||||||
|
verify_ssl=True,
|
||||||
|
raise_on_unexpected_status=True,
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def cli_create_user() -> None:
|
||||||
|
print(
|
||||||
|
"To set up Photos API connection you need to create a new user.\nIf you have email confirmation enabled in your Photos API config - you need to use a real email that will get a confirmation code afterwards.",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
username = input("Choose username for new Photos API user: ").strip()
|
||||||
|
email = input(f"Choose email for user '{username}': ").strip()
|
||||||
|
password = input(f"Choose password for user '{username}': ").strip()
|
||||||
|
try:
|
||||||
|
result_1 = await user_create(
|
||||||
|
client=unauthorized_client,
|
||||||
|
form_data=BodyUserCreateUsersPost(
|
||||||
|
user=username, email=email, password=password
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# asyncio.run(create_user(username, email, password))
|
||||||
|
await config_set("username", username, "posting", "api")
|
||||||
|
await config_set("password", password, "posting", "api")
|
||||||
|
none = input(
|
||||||
|
"Alright. If you have email confirmation enabled - please confirm registration by using the link in your email. After that press Enter. Otherwise just press Enter."
|
||||||
|
)
|
||||||
|
except Exception as exp:
|
||||||
|
print(f"Could not create a user due to {exp}", flush=True)
|
||||||
|
print_exc()
|
||||||
|
exit()
|
||||||
|
if not args.create_album:
|
||||||
|
print("You're done!", flush=True)
|
||||||
|
exit()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def cli_create_album() -> None:
|
||||||
|
print(
|
||||||
|
"To use Photos API your user needs to have an album to store its data.\nThis wizard will help you to create a new album with its name and title.",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
name = input("Choose a name for your album: ").strip()
|
||||||
|
title = input(f"Choose a title for album '{name}': ").strip()
|
||||||
|
try:
|
||||||
|
login_token = await login(
|
||||||
|
client=unauthorized_client,
|
||||||
|
form_data=BodyLoginForAccessTokenTokenPost(
|
||||||
|
grant_type="password",
|
||||||
|
scope="me albums.list albums.read albums.write photos.list photos.read photos.write videos.list videos.read videos.write",
|
||||||
|
username=await config_get("username", "posting", "api"),
|
||||||
|
password=await config_get("password", "posting", "api"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
client = AuthenticatedClient(
|
||||||
|
base_url=await config_get("address", "posting", "api"),
|
||||||
|
timeout=5.0,
|
||||||
|
verify_ssl=True,
|
||||||
|
raise_on_unexpected_status=True,
|
||||||
|
token=login_token.access_token,
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
result_2 = await album_create(client=client, name=name, title=title)
|
||||||
|
# asyncio.run(create_album(name, title))
|
||||||
|
await config_set("album", name, "posting", "api")
|
||||||
|
except Exception as exp:
|
||||||
|
print(f"Could not create an album due to {exp}", flush=True)
|
||||||
|
print_exc()
|
||||||
|
exit()
|
||||||
|
print("You're done!", flush=True)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
|
||||||
|
if args.create_user or args.create_album:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
tasks = []
|
||||||
|
|
||||||
|
if args.create_user:
|
||||||
|
loop.run_until_complete(asyncio.wait([loop.create_task(cli_create_user())]))
|
||||||
|
|
||||||
|
if args.create_album:
|
||||||
|
loop.run_until_complete(asyncio.wait([loop.create_task(cli_create_album())]))
|
||||||
|
|
||||||
|
loop.close()
|
@@ -1,22 +0,0 @@
|
|||||||
RESET = '\u001b[0m'
|
|
||||||
|
|
||||||
BLACK = '\u001b[30m'
|
|
||||||
RED = '\u001b[31m'
|
|
||||||
GREEN = '\u001b[32m'
|
|
||||||
YELLOW = '\u001b[33m'
|
|
||||||
BLUE = '\u001b[34m'
|
|
||||||
MAGENTA = '\u001b[35m'
|
|
||||||
CYAN = '\u001b[36m'
|
|
||||||
WHITE = '\u001b[37m'
|
|
||||||
|
|
||||||
BBLACK = '\u001b[30;1m'
|
|
||||||
BRED = '\u001b[31;1m'
|
|
||||||
BGREEN = '\u001b[32;1m'
|
|
||||||
BYELLOW = '\u001b[33;1m'
|
|
||||||
BBLUE = '\u001b[34;1m'
|
|
||||||
BMAGENTA = '\u001b[35;1m'
|
|
||||||
BCYAN = '\u001b[36;1m'
|
|
||||||
BWHITE = '\u001b[37;1m'
|
|
||||||
|
|
||||||
ULINE = '\u001b[4m'
|
|
||||||
REVERSE = '\u001b[7m'
|
|
34
modules/database.py
Normal file
34
modules/database.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
"""Module that provides all database columns"""
|
||||||
|
|
||||||
|
from pymongo import MongoClient
|
||||||
|
from ujson import loads
|
||||||
|
|
||||||
|
with open("config.json", "r", encoding="utf-8") as f:
|
||||||
|
db_config = loads(f.read())["database"]
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
if db_config["user"] is not None and db_config["password"] is not None:
|
||||||
|
con_string = "mongodb://{0}:{1}@{2}:{3}/{4}".format(
|
||||||
|
db_config["user"],
|
||||||
|
db_config["password"],
|
||||||
|
db_config["host"],
|
||||||
|
db_config["port"],
|
||||||
|
db_config["name"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
con_string = "mongodb://{0}:{1}/{2}".format(
|
||||||
|
db_config["host"], db_config["port"], db_config["name"]
|
||||||
|
)
|
||||||
|
|
||||||
|
db_client = MongoClient(con_string)
|
||||||
|
db = db_client.get_database(name=db_config["name"])
|
||||||
|
|
||||||
|
collections = db.list_collection_names()
|
||||||
|
|
||||||
|
for collection in ["sent", "users", "submitted"]:
|
||||||
|
if collection not in collections:
|
||||||
|
db.create_collection(collection)
|
||||||
|
|
||||||
|
col_sent = db.get_collection("sent")
|
||||||
|
col_users = db.get_collection("users")
|
||||||
|
col_submitted = db.get_collection("submitted")
|
6
modules/http_client.py
Normal file
6
modules/http_client.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from aiohttp import ClientSession
|
||||||
|
from ujson import dumps
|
||||||
|
|
||||||
|
http_session = ClientSession(
|
||||||
|
json_serialize=dumps,
|
||||||
|
)
|
@@ -1,62 +0,0 @@
|
|||||||
try:
|
|
||||||
from ujson import loads
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
from json import loads
|
|
||||||
|
|
||||||
from os import stat, makedirs, path, getcwd
|
|
||||||
from gzip import open as gzipopen
|
|
||||||
from shutil import copyfileobj
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
with open(getcwd()+path.sep+"config.json", "r", encoding='utf8') as file:
|
|
||||||
json_contents = loads(file.read())
|
|
||||||
log_size = json_contents["logging"]["size"]
|
|
||||||
log_folder = json_contents["logging"]["location"]
|
|
||||||
file.close()
|
|
||||||
|
|
||||||
# Check latest log size
|
|
||||||
def checkSize(debug=False):
|
|
||||||
|
|
||||||
global log_folder
|
|
||||||
|
|
||||||
if debug:
|
|
||||||
log_file = "debug.log"
|
|
||||||
else:
|
|
||||||
log_file = "latest.log"
|
|
||||||
|
|
||||||
try:
|
|
||||||
makedirs(log_folder, exist_ok=True)
|
|
||||||
log = stat(path.join(log_folder, log_file))
|
|
||||||
if (log.st_size / 1024) > log_size:
|
|
||||||
with open(path.join(log_folder, log_file), 'rb') as f_in:
|
|
||||||
with gzipopen(path.join(log_folder, f'{datetime.now().strftime("%d.%m.%Y_%H:%M:%S")}.log.gz'), 'wb') as f_out:
|
|
||||||
copyfileobj(f_in, f_out)
|
|
||||||
print(f'Copied {path.join(log_folder, datetime.now().strftime("%d.%m.%Y_%H:%M:%S"))}.log.gz')
|
|
||||||
open(path.join(log_folder, log_file), 'w').close()
|
|
||||||
except FileNotFoundError:
|
|
||||||
print(f'Log file {path.join(log_folder, log_file)} does not exist')
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Append string to log
|
|
||||||
def logAppend(message, debug=False):
|
|
||||||
|
|
||||||
global log_folder
|
|
||||||
|
|
||||||
message_formatted = f'[{datetime.now().strftime("%d.%m.%Y")}] [{datetime.now().strftime("%H:%M:%S")}] {message}'
|
|
||||||
checkSize(debug=debug)
|
|
||||||
|
|
||||||
if debug:
|
|
||||||
log_file = "debug.log"
|
|
||||||
else:
|
|
||||||
log_file = "latest.log"
|
|
||||||
|
|
||||||
log = open(path.join(log_folder, log_file), 'a')
|
|
||||||
log.write(f'{message_formatted}\n')
|
|
||||||
log.close()
|
|
||||||
|
|
||||||
# Print to stdout and then to log
|
|
||||||
def logWrite(message, debug=False):
|
|
||||||
# save to log file and rotation is to be done
|
|
||||||
logAppend(f'{message}', debug=debug)
|
|
||||||
print(f"{message}", flush=True)
|
|
3
modules/scheduler.py
Normal file
3
modules/scheduler.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
|
||||||
|
scheduler = AsyncIOScheduler()
|
270
modules/sender.py
Normal file
270
modules/sender.py
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from os import makedirs, path
|
||||||
|
from random import choice, sample
|
||||||
|
from shutil import rmtree
|
||||||
|
from traceback import format_exc, print_exc
|
||||||
|
from typing import Union
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
from libbot.pyrogram.classes import PyroClient
|
||||||
|
from photosapi_client.errors import UnexpectedStatus
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from modules.api_client import (
|
||||||
|
File,
|
||||||
|
PhotoSearch,
|
||||||
|
VideoSearch,
|
||||||
|
authorize,
|
||||||
|
client,
|
||||||
|
photo_get,
|
||||||
|
photo_patch,
|
||||||
|
photo_random,
|
||||||
|
video_get,
|
||||||
|
video_patch,
|
||||||
|
video_random,
|
||||||
|
)
|
||||||
|
from modules.database import col_sent, col_submitted
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_content(app: PyroClient, http_session: ClientSession) -> None:
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
token = await authorize(http_session)
|
||||||
|
except ValueError:
|
||||||
|
await app.send_message(
|
||||||
|
app.owner,
|
||||||
|
app._("api_creds_invalid", "message"),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
funcs = []
|
||||||
|
|
||||||
|
if app.config["posting"]["types"]["photo"]:
|
||||||
|
funcs.append((photo_random, photo_get, app.send_photo, photo_patch))
|
||||||
|
|
||||||
|
if app.config["posting"]["types"]["video"]:
|
||||||
|
funcs.append((video_random, video_get, app.send_video, video_patch))
|
||||||
|
|
||||||
|
if not funcs:
|
||||||
|
raise KeyError(
|
||||||
|
"No media source provided: all seem to be disabled in config"
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(funcs) > 1:
|
||||||
|
found = False
|
||||||
|
for func_iter in sample(funcs, len(funcs)):
|
||||||
|
func = func_iter
|
||||||
|
|
||||||
|
random_results = (
|
||||||
|
await func_iter[0](
|
||||||
|
album=app.config["posting"]["api"]["album"],
|
||||||
|
caption="queue",
|
||||||
|
client=client,
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
).results
|
||||||
|
|
||||||
|
if not random_results:
|
||||||
|
continue
|
||||||
|
|
||||||
|
media: Union[PhotoSearch, VideoSearch] = random_results[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
response: File = await func_iter[1](id=media.id, client=client)
|
||||||
|
except Exception as exp:
|
||||||
|
print_exc()
|
||||||
|
logger.error("Media is invalid: %s", exp)
|
||||||
|
if app.config["reports"]["error"]:
|
||||||
|
await app.send_message(
|
||||||
|
app.owner, f"Media is invalid: {exp}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
raise KeyError("No media found")
|
||||||
|
else:
|
||||||
|
func = funcs[0]
|
||||||
|
media: Union[PhotoSearch, VideoSearch] = (
|
||||||
|
await func[0](
|
||||||
|
album=app.config["posting"]["api"]["album"],
|
||||||
|
caption="queue",
|
||||||
|
client=client,
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
).results[0]
|
||||||
|
try:
|
||||||
|
response: File = await func[1](id=media.id, client=client)
|
||||||
|
except Exception as exp:
|
||||||
|
print_exc()
|
||||||
|
logger.error("Media is invalid: %s", exp)
|
||||||
|
if app.config["reports"]["error"]:
|
||||||
|
await app.send_message(app.owner, f"Media is invalid: {exp}")
|
||||||
|
return
|
||||||
|
|
||||||
|
except (KeyError, AttributeError, TypeError, IndexError):
|
||||||
|
logger.info(
|
||||||
|
"Could not send content due to queue empty or contains only forbidden extensions"
|
||||||
|
)
|
||||||
|
if app.config["reports"]["error"]:
|
||||||
|
await app.send_message(
|
||||||
|
app.owner,
|
||||||
|
app._("api_queue_empty", "message"),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
except (ValueError, UnexpectedStatus):
|
||||||
|
if app.config["reports"]["error"]:
|
||||||
|
await app.send_message(
|
||||||
|
app.owner,
|
||||||
|
app._("api_queue_error", "message"),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
tmp_dir = str(uuid4())
|
||||||
|
|
||||||
|
makedirs(path.join(app.config["locations"]["tmp"], tmp_dir), exist_ok=True)
|
||||||
|
|
||||||
|
tmp_path = path.join(tmp_dir, media.filename)
|
||||||
|
|
||||||
|
async with aiofiles.open(
|
||||||
|
path.join(app.config["locations"]["tmp"], tmp_path), "wb"
|
||||||
|
) as out_file:
|
||||||
|
await out_file.write(response.payload.read())
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Candidate %s (%s) is %s bytes big",
|
||||||
|
media.filename,
|
||||||
|
media.id,
|
||||||
|
path.getsize(path.join(app.config["locations"]["tmp"], tmp_path)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
path.getsize(path.join(app.config["locations"]["tmp"], tmp_path)) > 5242880
|
||||||
|
) and func[0] is photo_random:
|
||||||
|
image = Image.open(path.join(app.config["locations"]["tmp"], tmp_path))
|
||||||
|
width, height = image.size
|
||||||
|
image = image.resize((int(width / 2), int(height / 2)), Image.ANTIALIAS)
|
||||||
|
if tmp_path.lower().endswith(".jpeg") or tmp_path.lower().endswith(".jpg"):
|
||||||
|
image.save(
|
||||||
|
path.join(app.config["locations"]["tmp"], tmp_path),
|
||||||
|
"JPEG",
|
||||||
|
optimize=True,
|
||||||
|
quality=50,
|
||||||
|
)
|
||||||
|
elif tmp_path.lower().endswith(".png"):
|
||||||
|
image.save(
|
||||||
|
path.join(app.config["locations"]["tmp"], tmp_path),
|
||||||
|
"PNG",
|
||||||
|
optimize=True,
|
||||||
|
compress_level=8,
|
||||||
|
)
|
||||||
|
image.close()
|
||||||
|
|
||||||
|
if (
|
||||||
|
path.getsize(path.join(app.config["locations"]["tmp"], tmp_path)) > 5242880
|
||||||
|
) and func[0] is photo_random:
|
||||||
|
rmtree(
|
||||||
|
path.join(app.config["locations"]["tmp"], tmp_dir), ignore_errors=True
|
||||||
|
)
|
||||||
|
raise BytesWarning
|
||||||
|
|
||||||
|
del response
|
||||||
|
|
||||||
|
submitted = col_submitted.find_one({"temp.file": media.filename})
|
||||||
|
|
||||||
|
if submitted is not None and submitted["caption"] is not None:
|
||||||
|
caption = submitted["caption"].strip()
|
||||||
|
else:
|
||||||
|
caption = ""
|
||||||
|
|
||||||
|
if (
|
||||||
|
submitted is not None
|
||||||
|
and app.config["posting"]["submitted_caption"]["enabled"]
|
||||||
|
and (
|
||||||
|
(submitted["user"] not in app.admins)
|
||||||
|
or (
|
||||||
|
app.config["posting"]["submitted_caption"]["ignore_admins"] is False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
caption = (
|
||||||
|
f"{caption}\n\n{app.config['posting']['submitted_caption']['text']}\n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
caption = f"{caption}\n\n"
|
||||||
|
|
||||||
|
if app.config["caption"]["enabled"]:
|
||||||
|
if app.config["caption"]["link"] is not None:
|
||||||
|
caption = f"{caption}[{choice(app.config['caption']['text'])}]({app.config['caption']['link']})"
|
||||||
|
else:
|
||||||
|
caption = f"{caption}{choice(app.config['caption']['text'])}"
|
||||||
|
else:
|
||||||
|
caption = caption
|
||||||
|
|
||||||
|
try:
|
||||||
|
sent = await func[2](
|
||||||
|
app.config["posting"]["channel"],
|
||||||
|
path.join(app.config["locations"]["tmp"], tmp_path),
|
||||||
|
caption=caption,
|
||||||
|
disable_notification=app.config["posting"]["silent"],
|
||||||
|
)
|
||||||
|
except Exception as exp:
|
||||||
|
logger.error(
|
||||||
|
"Could not send media %s (%s) due to %s", media.filename, media.id, exp
|
||||||
|
)
|
||||||
|
if app.config["reports"]["error"]:
|
||||||
|
await app.send_message(
|
||||||
|
app.owner,
|
||||||
|
app._("post_exception", "message").format(exp, format_exc()),
|
||||||
|
)
|
||||||
|
# rmtree(path.join(app.config['locations']['tmp'], tmp_dir), ignore_errors=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
col_sent.insert_one(
|
||||||
|
{
|
||||||
|
"date": datetime.now(),
|
||||||
|
"image": media.id,
|
||||||
|
"filename": media.filename,
|
||||||
|
"channel": app.config["posting"]["channel"],
|
||||||
|
"caption": None
|
||||||
|
if (submitted is None or submitted["caption"] is None)
|
||||||
|
else submitted["caption"].strip(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await func[3](id=media.id, client=client, caption="sent")
|
||||||
|
|
||||||
|
rmtree(path.join(app.config["locations"]["tmp"], tmp_dir), ignore_errors=True)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Sent %s to %s with caption %s and silently %s",
|
||||||
|
media.id,
|
||||||
|
str(app.config["posting"]["channel"]),
|
||||||
|
caption.replace("\n", "%n"),
|
||||||
|
str(app.config["posting"]["silent"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as exp:
|
||||||
|
logger.error(
|
||||||
|
"Could not send content due to %s. Traceback: %s", exp, format_exc()
|
||||||
|
)
|
||||||
|
if app.config["reports"]["error"]:
|
||||||
|
await app.send_message(
|
||||||
|
app.owner,
|
||||||
|
app._("post_exception", "message").format(exp, format_exc()),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
rmtree(
|
||||||
|
path.join(app.config["locations"]["tmp"], tmp_dir), ignore_errors=True
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
pass
|
137
modules/utils.py
137
modules/utils.py
@@ -1,116 +1,33 @@
|
|||||||
try:
|
import logging
|
||||||
from ujson import JSONDecodeError as JSONDecodeError
|
from os import makedirs, path
|
||||||
from ujson import loads, dumps
|
from pathlib import Path
|
||||||
except ModuleNotFoundError:
|
from typing import List, Union
|
||||||
from json import JSONDecodeError as JSONDecodeError
|
from zipfile import ZipFile
|
||||||
from json import loads, dumps
|
|
||||||
|
|
||||||
import os
|
import aiofiles
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from signal import SIGKILL # type: ignore
|
logger = logging.getLogger(__name__)
|
||||||
from modules.logging import logWrite
|
|
||||||
|
|
||||||
def jsonLoad(filename):
|
USERS_WITH_CONTEXT: List[int] = []
|
||||||
"""Loads arg1 as json and returns its contents"""
|
|
||||||
with open(filename, "r", encoding='utf8') as file:
|
|
||||||
try:
|
|
||||||
output = loads(file.read())
|
|
||||||
except JSONDecodeError:
|
|
||||||
logWrite(f"Could not load json file {filename}: file seems to be incorrect!\n{traceback.print_exc()}")
|
|
||||||
raise
|
|
||||||
except FileNotFoundError:
|
|
||||||
logWrite(f"Could not load json file {filename}: file does not seem to exist!\n{traceback.print_exc()}")
|
|
||||||
raise
|
|
||||||
file.close()
|
|
||||||
return output
|
|
||||||
|
|
||||||
def jsonSave(contents, filename):
|
|
||||||
"""Dumps dict/list arg1 to file arg2"""
|
async def extract_and_save(handle: ZipFile, filename: str, destpath: Union[str, Path]):
|
||||||
|
"""Extract and save file from archive
|
||||||
|
|
||||||
|
### Args:
|
||||||
|
* handle (`ZipFile`): ZipFile handler
|
||||||
|
* filename (`str`): File base name
|
||||||
|
* path (`Union[str, Path]`): Path where to store
|
||||||
|
"""
|
||||||
|
data = handle.read(filename)
|
||||||
|
filepath = path.join(str(destpath), filename)
|
||||||
try:
|
try:
|
||||||
with open(filename, "w", encoding='utf8') as file:
|
makedirs(path.dirname(filepath), exist_ok=True)
|
||||||
file.write(dumps(contents, ensure_ascii=False, indent=4))
|
async with aiofiles.open(filepath, "wb") as fd:
|
||||||
file.close()
|
await fd.write(data)
|
||||||
except Exception as exp:
|
logger.debug("Unzipped %s", filename)
|
||||||
logWrite(f"Could not save json file {filename}: {exp}\n{traceback.print_exc()}")
|
except IsADirectoryError:
|
||||||
return
|
makedirs(filepath, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def configSet(key: str, value, *args: str):
|
|
||||||
"""Set key to a value
|
|
||||||
Args:
|
|
||||||
* key (str): The last key of the keys path.
|
|
||||||
* value (str/int/float/list/dict/None): Some needed value.
|
|
||||||
* *args (str): Path to key like: dict[args][key].
|
|
||||||
"""
|
|
||||||
this_dict = jsonLoad("config.json")
|
|
||||||
string = "this_dict"
|
|
||||||
for arg in args:
|
|
||||||
string += f'["{arg}"]'
|
|
||||||
if type(value) in [str]:
|
|
||||||
string += f'["{key}"] = "{value}"'
|
|
||||||
else:
|
|
||||||
string += f'["{key}"] = {value}'
|
|
||||||
exec(string)
|
|
||||||
jsonSave(this_dict, "config.json")
|
|
||||||
return
|
|
||||||
|
|
||||||
def configGet(key: str, *args: str):
|
|
||||||
"""Get value of the config key
|
|
||||||
Args:
|
|
||||||
* key (str): The last key of the keys path.
|
|
||||||
* *args (str): Path to key like: dict[args][key].
|
|
||||||
Returns:
|
|
||||||
* any: Value of provided key
|
|
||||||
"""
|
|
||||||
this_dict = jsonLoad("config.json")
|
|
||||||
this_key = this_dict
|
|
||||||
for dict_key in args:
|
|
||||||
this_key = this_key[dict_key]
|
|
||||||
return this_key[key]
|
|
||||||
|
|
||||||
def locale(key: str, *args: str, locale=configGet("locale")):
|
|
||||||
"""Get value of locale string
|
|
||||||
Args:
|
|
||||||
* key (str): The last key of the locale's keys path.
|
|
||||||
* *args (list): Path to key like: dict[args][key].
|
|
||||||
* locale (str): Locale to looked up in. Defaults to config's locale value.
|
|
||||||
Returns:
|
|
||||||
* any: Value of provided locale key
|
|
||||||
"""
|
|
||||||
if (locale == None):
|
|
||||||
locale = configGet("locale")
|
|
||||||
|
|
||||||
try:
|
|
||||||
this_dict = jsonLoad(f'{configGet("locale", "locations")}{os.sep}{locale}.json')
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
try:
|
pass
|
||||||
this_dict = jsonLoad(f'{configGet("locale", "locations")}{os.sep}{configGet("locale")}.json')
|
return
|
||||||
except FileNotFoundError:
|
|
||||||
try:
|
|
||||||
this_dict = jsonLoad(f'{configGet("locale_fallback", "locations")}{os.sep}{configGet("locale")}.json')
|
|
||||||
except:
|
|
||||||
return f'⚠️ Locale in config is invalid: could not get "{key}" in {str(args)} from locale "{locale}"'
|
|
||||||
|
|
||||||
this_key = this_dict
|
|
||||||
for dict_key in args:
|
|
||||||
this_key = this_key[dict_key]
|
|
||||||
|
|
||||||
try:
|
|
||||||
return this_key[key]
|
|
||||||
except KeyError:
|
|
||||||
return f'⚠️ Locale in config is invalid: could not get "{key}" in {str(args)} from locale "{locale}"'
|
|
||||||
|
|
||||||
try:
|
|
||||||
import psutil
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
print(locale("deps_missing", "console", locale=configGet("locale")), flush=True)
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
def killProc(pid):
|
|
||||||
if os.name == "posix":
|
|
||||||
os.kill(pid, SIGKILL)
|
|
||||||
else:
|
|
||||||
p = psutil.Process(pid)
|
|
||||||
p.kill()
|
|
||||||
|
12
plugins/callbacks/nothing.py
Normal file
12
plugins/callbacks/nothing.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from pyrogram import filters
|
||||||
|
from pyrogram.client import Client
|
||||||
|
from pyrogram.types import CallbackQuery
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_callback_query(filters.regex("nothing"))
|
||||||
|
async def callback_query_nothing(app: PyroClient, clb: CallbackQuery):
|
||||||
|
user = await app.find_user(clb.from_user)
|
||||||
|
|
||||||
|
await clb.answer(text=app._("nothing", "callback", locale=user.locale))
|
25
plugins/callbacks/shutdown.py
Normal file
25
plugins/callbacks/shutdown.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from os import makedirs, path
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from libbot import config_get, json_write
|
||||||
|
from pyrogram import filters
|
||||||
|
from pyrogram.client import Client
|
||||||
|
from pyrogram.types import CallbackQuery
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_callback_query(filters.regex("shutdown"))
|
||||||
|
async def callback_query_nothing(app: PyroClient, clb: CallbackQuery):
|
||||||
|
if clb.from_user.id not in app.admins:
|
||||||
|
return
|
||||||
|
|
||||||
|
await clb.answer()
|
||||||
|
|
||||||
|
makedirs(await config_get("cache", "locations"), exist_ok=True)
|
||||||
|
await json_write(
|
||||||
|
{"timestamp": time()},
|
||||||
|
path.join(await config_get("cache", "locations"), "shutdown_time"),
|
||||||
|
)
|
||||||
|
|
||||||
|
exit()
|
266
plugins/callbacks/submission.py
Normal file
266
plugins/callbacks/submission.py
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
import logging
|
||||||
|
from os import path
|
||||||
|
from pathlib import Path
|
||||||
|
from shutil import rmtree
|
||||||
|
|
||||||
|
from bson import ObjectId
|
||||||
|
from libbot import config_get
|
||||||
|
from pyrogram import filters
|
||||||
|
from pyrogram.client import Client
|
||||||
|
from pyrogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
|
|
||||||
|
from classes.exceptions import (
|
||||||
|
SubmissionDuplicatesError,
|
||||||
|
SubmissionUnavailableError,
|
||||||
|
SubmissionUnsupportedError,
|
||||||
|
)
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules.database import col_submitted
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_callback_query(filters.regex("sub_yes_[\s\S]*"))
|
||||||
|
async def callback_query_yes(app: PyroClient, clb: CallbackQuery):
|
||||||
|
user = await app.find_user(clb.from_user)
|
||||||
|
fullclb = str(clb.data).split("_")
|
||||||
|
|
||||||
|
db_entry = col_submitted.find_one({"_id": ObjectId(fullclb[2])})
|
||||||
|
|
||||||
|
try:
|
||||||
|
submission = await app.submit_media(fullclb[2])
|
||||||
|
except SubmissionUnavailableError:
|
||||||
|
await clb.answer(
|
||||||
|
text=app._("sub_msg_unavail", "callback", locale=user.locale),
|
||||||
|
show_alert=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except SubmissionUnsupportedError:
|
||||||
|
await clb.answer(
|
||||||
|
text=app._("mime_not_allowed", "message", locale=user.locale).format(
|
||||||
|
", ".join(app.config["submission"]["mime_types"]), quote=True
|
||||||
|
),
|
||||||
|
show_alert=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except SubmissionDuplicatesError as exp:
|
||||||
|
await clb.answer(
|
||||||
|
text=app._("sub_duplicates_found", "callback", locale=user.locale),
|
||||||
|
show_alert=True,
|
||||||
|
)
|
||||||
|
await clb.message.reply_text(
|
||||||
|
app._("sub_media_duplicates_list", "message", locale=user.locale).format(
|
||||||
|
"\n • ".join(exp.duplicates)
|
||||||
|
),
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"Submission with ID '%s' could not be accepted because of the duplicates: %s",
|
||||||
|
fullclb[2],
|
||||||
|
str(exp.duplicates),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if submission[0] is not None:
|
||||||
|
await submission[0].reply_text(
|
||||||
|
app._(
|
||||||
|
"sub_yes",
|
||||||
|
"message",
|
||||||
|
locale=(await app.find_user(submission[0].from_user)).locale,
|
||||||
|
),
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
elif db_entry is not None:
|
||||||
|
await app.send_message(
|
||||||
|
db_entry["user"],
|
||||||
|
app._(
|
||||||
|
"sub_yes",
|
||||||
|
"message",
|
||||||
|
locale=(await app.find_user(db_entry["user"])).locale,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await clb.answer(
|
||||||
|
text=app._("sub_yes", "callback", locale=user.locale).format(fullclb[2]),
|
||||||
|
show_alert=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
edited_markup = (
|
||||||
|
[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=str(app._("accepted", "button", locale=user.locale)),
|
||||||
|
callback_data="nothing",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
clb.message.reply_markup.inline_keyboard[1],
|
||||||
|
]
|
||||||
|
if len(clb.message.reply_markup.inline_keyboard) > 1
|
||||||
|
else [
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=str(app._("accepted", "button", locale=user.locale)),
|
||||||
|
callback_data="nothing",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if await config_get("send_uploaded_id", "submission"):
|
||||||
|
await clb.message.edit_caption(
|
||||||
|
f"{clb.message.caption}\n\nID: `{submission[1]}`"
|
||||||
|
)
|
||||||
|
|
||||||
|
await clb.message.edit_reply_markup(
|
||||||
|
reply_markup=InlineKeyboardMarkup(edited_markup)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Submission with ID '%s' accepted and uploaded with ID '%s'",
|
||||||
|
fullclb[2],
|
||||||
|
submission[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_callback_query(filters.regex("sub_no_[\s\S]*"))
|
||||||
|
async def callback_query_no(app: PyroClient, clb: CallbackQuery):
|
||||||
|
user = await app.find_user(clb.from_user)
|
||||||
|
fullclb = str(clb.data).split("_")
|
||||||
|
|
||||||
|
db_entry = col_submitted.find_one_and_delete({"_id": ObjectId(fullclb[2])})
|
||||||
|
|
||||||
|
if (
|
||||||
|
db_entry["temp"]["uuid"] is not None
|
||||||
|
and Path(
|
||||||
|
f"{app.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}"
|
||||||
|
).exists()
|
||||||
|
):
|
||||||
|
rmtree(
|
||||||
|
Path(
|
||||||
|
f"{app.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}"
|
||||||
|
),
|
||||||
|
ignore_errors=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
submission = await app.get_messages(
|
||||||
|
db_entry["user"], db_entry["telegram"]["msg_id"]
|
||||||
|
)
|
||||||
|
except Exception as exp:
|
||||||
|
await clb.answer(
|
||||||
|
text=app._("sub_msg_unavail", "message", locale=user.locale),
|
||||||
|
show_alert=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await submission.reply_text(
|
||||||
|
app._(
|
||||||
|
"sub_no",
|
||||||
|
"message",
|
||||||
|
locale=(await app.find_user(submission.from_user)).locale,
|
||||||
|
),
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
await clb.answer(
|
||||||
|
text=app._("sub_no", "callback", locale=user.locale).format(fullclb[2]),
|
||||||
|
show_alert=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
edited_markup = (
|
||||||
|
[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=str(app._("declined", "button", locale=user.locale)),
|
||||||
|
callback_data="nothing",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
clb.message.reply_markup.inline_keyboard[1],
|
||||||
|
]
|
||||||
|
if len(clb.message.reply_markup.inline_keyboard) > 1
|
||||||
|
else [
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=str(app._("declined", "button", locale=user.locale)),
|
||||||
|
callback_data="nothing",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
await clb.message.edit_reply_markup(
|
||||||
|
reply_markup=InlineKeyboardMarkup(edited_markup)
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"Submission with ID '%s' rejected",
|
||||||
|
fullclb[2],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_callback_query(filters.regex("sub_block_[\s\S]*"))
|
||||||
|
async def callback_query_block(app: PyroClient, clb: CallbackQuery):
|
||||||
|
user = await app.find_user(clb.from_user)
|
||||||
|
fullclb = str(clb.data).split("_")
|
||||||
|
|
||||||
|
await app.send_message(
|
||||||
|
int(fullclb[2]),
|
||||||
|
app._(
|
||||||
|
"sub_blocked",
|
||||||
|
"message",
|
||||||
|
locale=(await app.find_user(int(fullclb[2]))).locale,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await user.block()
|
||||||
|
|
||||||
|
await clb.answer(
|
||||||
|
text=app._("sub_block", "callback", locale=user.locale).format(fullclb[2]),
|
||||||
|
show_alert=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
edited_markup = [
|
||||||
|
clb.message.reply_markup.inline_keyboard[0],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=str(app._("sub_unblock", "button", locale=user.locale)),
|
||||||
|
callback_data=f"sub_unblock_{fullclb[2]}",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
]
|
||||||
|
await clb.message.edit_reply_markup(
|
||||||
|
reply_markup=InlineKeyboardMarkup(edited_markup)
|
||||||
|
)
|
||||||
|
logger.info("User %s has been blocked", fullclb[2])
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_callback_query(filters.regex("sub_unblock_[\s\S]*"))
|
||||||
|
async def callback_query_unblock(app: PyroClient, clb: CallbackQuery):
|
||||||
|
user = await app.find_user(clb.from_user)
|
||||||
|
fullclb = str(clb.data).split("_")
|
||||||
|
|
||||||
|
await app.send_message(
|
||||||
|
int(fullclb[2]),
|
||||||
|
app._(
|
||||||
|
"sub_unblocked",
|
||||||
|
"message",
|
||||||
|
locale=(await app.find_user(int(fullclb[2]))).locale,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await user.unblock()
|
||||||
|
|
||||||
|
await clb.answer(
|
||||||
|
text=app._("sub_unblock", "callback", locale=user.locale).format(fullclb[2]),
|
||||||
|
show_alert=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
edited_markup = [
|
||||||
|
clb.message.reply_markup.inline_keyboard[0],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=str(app._("sub_block", "button", locale=user.locale)),
|
||||||
|
callback_data=f"sub_block_{fullclb[2]}",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
]
|
||||||
|
await clb.message.edit_reply_markup(
|
||||||
|
reply_markup=InlineKeyboardMarkup(edited_markup)
|
||||||
|
)
|
||||||
|
logger.info("User %s has been unblocked", fullclb[2])
|
48
plugins/commands/general.py
Normal file
48
plugins/commands/general.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import asyncio
|
||||||
|
from os import makedirs
|
||||||
|
from pathlib import Path
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from libbot import json_write
|
||||||
|
from pyrogram import filters
|
||||||
|
from pyrogram.client import Client
|
||||||
|
from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules.utils import USERS_WITH_CONTEXT
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_message(
|
||||||
|
~filters.scheduled & filters.command(["shutdown"], prefixes=["", "/"])
|
||||||
|
)
|
||||||
|
async def cmd_kill(app: PyroClient, msg: Message):
|
||||||
|
if msg.from_user.id not in app.admins:
|
||||||
|
return
|
||||||
|
|
||||||
|
user = await app.find_user(msg.from_user)
|
||||||
|
|
||||||
|
if len(USERS_WITH_CONTEXT) > 0:
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("shutdown_confirm", "message", locale=user.locale).format(
|
||||||
|
len(USERS_WITH_CONTEXT)
|
||||||
|
),
|
||||||
|
reply_markup=InlineKeyboardMarkup(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
app._("shutdown", "button", locale=user.locale),
|
||||||
|
callback_data="shutdown",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
makedirs(app.config["locations"]["cache"], exist_ok=True)
|
||||||
|
await json_write(
|
||||||
|
{"timestamp": time()},
|
||||||
|
Path(f"{app.config['locations']['cache']}/shutdown_time"),
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.get_event_loop().create_task(app.stop())
|
27
plugins/commands/mode_submit.py
Normal file
27
plugins/commands/mode_submit.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from pyrogram import filters
|
||||||
|
from pyrogram.client import Client
|
||||||
|
from pyrogram.types import Message
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_message(~filters.scheduled & filters.command(["start"], prefixes="/"))
|
||||||
|
async def cmd_start(app: PyroClient, msg: Message):
|
||||||
|
user = await app.find_user(msg.from_user)
|
||||||
|
|
||||||
|
if user.banned:
|
||||||
|
return
|
||||||
|
|
||||||
|
await msg.reply_text(app._("start", "message", locale=user.locale))
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_message(
|
||||||
|
~filters.scheduled & filters.command(["rules", "help"], prefixes="/")
|
||||||
|
)
|
||||||
|
async def cmd_rules(app: PyroClient, msg: Message):
|
||||||
|
user = await app.find_user(msg.from_user)
|
||||||
|
|
||||||
|
if user.banned:
|
||||||
|
return
|
||||||
|
|
||||||
|
await msg.reply_text(app._("rules", "message", locale=user.locale))
|
356
plugins/commands/photos.py
Normal file
356
plugins/commands/photos.py
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from glob import iglob
|
||||||
|
from io import BytesIO
|
||||||
|
from os import getcwd, makedirs, path, remove
|
||||||
|
from pathlib import Path
|
||||||
|
from shutil import disk_usage, rmtree
|
||||||
|
from traceback import format_exc
|
||||||
|
from uuid import uuid4
|
||||||
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
from convopyro import listen_message
|
||||||
|
from photosapi_client.errors import UnexpectedStatus
|
||||||
|
from pyrogram import filters
|
||||||
|
from pyrogram.client import Client
|
||||||
|
from pyrogram.types import (
|
||||||
|
KeyboardButton,
|
||||||
|
Message,
|
||||||
|
ReplyKeyboardMarkup,
|
||||||
|
ReplyKeyboardRemove,
|
||||||
|
)
|
||||||
|
from ujson import loads
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules.api_client import (
|
||||||
|
BodyPhotoUpload,
|
||||||
|
File,
|
||||||
|
client,
|
||||||
|
photo_delete,
|
||||||
|
photo_upload,
|
||||||
|
video_delete,
|
||||||
|
video_upload,
|
||||||
|
)
|
||||||
|
from modules.utils import USERS_WITH_CONTEXT, extract_and_save
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_message(~filters.scheduled & filters.command(["import"], prefixes=["", "/"]))
|
||||||
|
async def cmd_import(app: PyroClient, msg: Message):
|
||||||
|
if msg.from_user.id not in app.admins:
|
||||||
|
return
|
||||||
|
|
||||||
|
global USERS_WITH_CONTEXT
|
||||||
|
|
||||||
|
if msg.from_user.id not in USERS_WITH_CONTEXT:
|
||||||
|
USERS_WITH_CONTEXT.append(msg.from_user.id)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
user = await app.find_user(msg.from_user)
|
||||||
|
|
||||||
|
await msg.reply_text(app._("import_request", "message", locale=user.locale))
|
||||||
|
|
||||||
|
answer = await listen_message(app, msg.chat.id, timeout=600)
|
||||||
|
|
||||||
|
USERS_WITH_CONTEXT.remove(msg.from_user.id)
|
||||||
|
|
||||||
|
if answer is None:
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("import_ignored", "message", locale=user.locale),
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if answer.text == "/cancel":
|
||||||
|
await answer.reply_text(app._("import_abort", "message", locale=user.locale))
|
||||||
|
return
|
||||||
|
|
||||||
|
if answer.document is None:
|
||||||
|
await answer.reply_text(
|
||||||
|
app._(
|
||||||
|
"import_invalid_media",
|
||||||
|
"message",
|
||||||
|
locale=user.locale,
|
||||||
|
),
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if answer.document.mime_type != "application/zip":
|
||||||
|
await answer.reply_text(
|
||||||
|
app._("import_invalid_mime", "message", locale=user.locale),
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if disk_usage(getcwd())[2] < (answer.document.file_size) * 3:
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("import_too_big", "message", locale=user.locale).format(
|
||||||
|
answer.document.file_size // (2**30),
|
||||||
|
disk_usage(getcwd())[2] // (2**30),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
tmp_dir = str(uuid4())
|
||||||
|
|
||||||
|
logging.info(
|
||||||
|
"Importing '%s' file %s bytes big (TMP ID %s)",
|
||||||
|
answer.document.file_name,
|
||||||
|
answer.document.file_size,
|
||||||
|
tmp_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
makedirs(Path(f"{app.config['locations']['tmp']}/{tmp_dir}"), exist_ok=True)
|
||||||
|
tmp_path = Path(f"{app.config['locations']['tmp']}/{answer.document.file_id}")
|
||||||
|
|
||||||
|
downloading = await answer.reply_text(
|
||||||
|
app._("import_downloading", "message", locale=user.locale),
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
await app.download_media(answer, file_name=str(tmp_path))
|
||||||
|
await downloading.edit(app._("import_unpacking", "message", locale=user.locale))
|
||||||
|
|
||||||
|
try:
|
||||||
|
with ZipFile(tmp_path, "r") as handle:
|
||||||
|
tasks = [
|
||||||
|
extract_and_save(
|
||||||
|
handle, name, Path(f"{app.config['locations']['tmp']}/{tmp_dir}")
|
||||||
|
)
|
||||||
|
for name in handle.namelist()
|
||||||
|
]
|
||||||
|
_ = await asyncio.gather(*tasks)
|
||||||
|
except Exception as exp:
|
||||||
|
logger.error(
|
||||||
|
"Could not import '%s' due to %s: %s",
|
||||||
|
answer.document.file_name,
|
||||||
|
exp,
|
||||||
|
format_exc(),
|
||||||
|
)
|
||||||
|
await answer.reply_text(
|
||||||
|
app._("import_unpack_error", "message", locale=user.locale).format(
|
||||||
|
exp, format_exc()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Downloaded '%s' - awaiting upload", answer.document.file_name)
|
||||||
|
|
||||||
|
await downloading.edit(app._("import_uploading", "message", locale=user.locale))
|
||||||
|
|
||||||
|
remove(tmp_path)
|
||||||
|
|
||||||
|
for filename in iglob(
|
||||||
|
str(Path(f"{app.config['locations']['tmp']}/{tmp_dir}")) + "**/**",
|
||||||
|
recursive=True,
|
||||||
|
):
|
||||||
|
if not path.isfile(filename):
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(str(filename), "rb") as fh:
|
||||||
|
photo_bytes = BytesIO(fh.read())
|
||||||
|
|
||||||
|
try:
|
||||||
|
# VIDEO SUPPORT IS PLANNED HERE TOO
|
||||||
|
uploaded = await photo_upload(
|
||||||
|
app.config["posting"]["api"]["album"],
|
||||||
|
client=client,
|
||||||
|
multipart_data=BodyPhotoUpload(
|
||||||
|
File(photo_bytes, Path(filename).name, "image/jpeg")
|
||||||
|
),
|
||||||
|
ignore_duplicates=app.config["submission"]["allow_duplicates"],
|
||||||
|
compress=False,
|
||||||
|
caption="queue",
|
||||||
|
)
|
||||||
|
except UnexpectedStatus as exp:
|
||||||
|
logger.error(
|
||||||
|
"Could not upload '%s' from '%s': %s",
|
||||||
|
filename,
|
||||||
|
Path(f"{app.config['locations']['tmp']}/{tmp_dir}"),
|
||||||
|
exp,
|
||||||
|
)
|
||||||
|
await msg.reply_text(
|
||||||
|
app._(
|
||||||
|
"import_upload_error_other",
|
||||||
|
"message",
|
||||||
|
locale=user.locale,
|
||||||
|
).format(path.basename(filename)),
|
||||||
|
disable_notification=True,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
uploaded_dict = loads(uploaded.content.decode("utf-8"))
|
||||||
|
|
||||||
|
if "duplicates" in uploaded_dict:
|
||||||
|
logger.warning(
|
||||||
|
"Could not upload '%s' from '%s'. Duplicates: %s",
|
||||||
|
filename,
|
||||||
|
Path(f"{app.config['locations']['tmp']}/{tmp_dir}"),
|
||||||
|
str(uploaded_dict["duplicates"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(uploaded_dict["duplicates"]) > 0:
|
||||||
|
await msg.reply_text(
|
||||||
|
app._(
|
||||||
|
"import_upload_error_duplicate",
|
||||||
|
"message",
|
||||||
|
locale=user.locale,
|
||||||
|
).format(path.basename(filename)),
|
||||||
|
disable_notification=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await msg.reply_text(
|
||||||
|
app._(
|
||||||
|
"import_upload_error_other",
|
||||||
|
"message",
|
||||||
|
locale=user.locale,
|
||||||
|
).format(path.basename(filename)),
|
||||||
|
disable_notification=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"Uploaded '%s' from '%s' and got ID %s",
|
||||||
|
filename,
|
||||||
|
Path(f"{app.config['locations']['tmp']}/{tmp_dir}"),
|
||||||
|
uploaded.parsed.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
await downloading.delete()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Removing '%s' after uploading",
|
||||||
|
Path(f"{app.config['locations']['tmp']}/{tmp_dir}"),
|
||||||
|
)
|
||||||
|
rmtree(Path(f"{app.config['locations']['tmp']}/{tmp_dir}"), ignore_errors=True)
|
||||||
|
|
||||||
|
await answer.reply_text(
|
||||||
|
app._("import_finished", "message", locale=user.locale),
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_message(~filters.scheduled & filters.command(["export"], prefixes=["", "/"]))
|
||||||
|
async def cmd_export(app: PyroClient, msg: Message):
|
||||||
|
if msg.from_user.id not in app.admins:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_message(~filters.scheduled & filters.command(["remove"], prefixes=["", "/"]))
|
||||||
|
async def cmd_remove(app: PyroClient, msg: Message):
|
||||||
|
if msg.from_user.id not in app.admins:
|
||||||
|
return
|
||||||
|
|
||||||
|
global USERS_WITH_CONTEXT
|
||||||
|
|
||||||
|
if msg.from_user.id not in USERS_WITH_CONTEXT:
|
||||||
|
USERS_WITH_CONTEXT.append(msg.from_user.id)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
user = await app.find_user(msg.from_user)
|
||||||
|
|
||||||
|
await msg.reply_text(app._("remove_request", "message", locale=user.locale))
|
||||||
|
|
||||||
|
answer_id = await listen_message(app, msg.chat.id, timeout=600)
|
||||||
|
|
||||||
|
USERS_WITH_CONTEXT.remove(msg.from_user.id)
|
||||||
|
|
||||||
|
if answer_id is None:
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("remove_ignored", "message", locale=user.locale),
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if answer_id.text == "/cancel":
|
||||||
|
await answer_id.reply_text(app._("remove_abort", "message", locale=user.locale))
|
||||||
|
return
|
||||||
|
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("remove_kind", "message", locale=user.locale),
|
||||||
|
reply_markup=ReplyKeyboardMarkup(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
KeyboardButton(app._("photo", "button", locale=user.locale)),
|
||||||
|
KeyboardButton(app._("video", "button", locale=user.locale)),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
resize_keyboard=True,
|
||||||
|
one_time_keyboard=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
USERS_WITH_CONTEXT.append(msg.from_user.id)
|
||||||
|
|
||||||
|
answer_kind = await listen_message(app, msg.chat.id, timeout=600)
|
||||||
|
|
||||||
|
USERS_WITH_CONTEXT.remove(msg.from_user.id)
|
||||||
|
|
||||||
|
if answer_kind is None:
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("remove_ignored", "message", locale=user.locale),
|
||||||
|
quote=True,
|
||||||
|
reply_markup=ReplyKeyboardRemove(),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if answer_kind.text == "/cancel":
|
||||||
|
await answer_kind.reply_text(
|
||||||
|
app._("remove_abort", "message", locale=user.locale),
|
||||||
|
reply_markup=ReplyKeyboardRemove(),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if answer_kind.text in app.in_all_locales("photo", "button"):
|
||||||
|
func = photo_delete
|
||||||
|
elif answer_kind.text in app.in_all_locales("video", "button"):
|
||||||
|
func = video_delete
|
||||||
|
else:
|
||||||
|
await answer_kind.reply_text(
|
||||||
|
app._("remove_unknown", "message", locale=user.locale).format(
|
||||||
|
app._("photo", "button", locale=user.locale),
|
||||||
|
app._("video", "button", locale=user.locale),
|
||||||
|
),
|
||||||
|
reply_markup=ReplyKeyboardRemove(),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
response = await func(id=answer_id.text, client=client)
|
||||||
|
|
||||||
|
if response is None:
|
||||||
|
logger.info(
|
||||||
|
"Removed %s '%s' by request of user %s",
|
||||||
|
answer_kind.text,
|
||||||
|
answer_id.text,
|
||||||
|
answer_id.from_user.id,
|
||||||
|
)
|
||||||
|
await answer_kind.reply_text(
|
||||||
|
app._("remove_success", "message", locale=user.locale).format(
|
||||||
|
answer_id.text
|
||||||
|
),
|
||||||
|
reply_markup=ReplyKeyboardRemove(),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Could not remove %s '%s' by request of user %s",
|
||||||
|
answer_kind.text,
|
||||||
|
answer_id.text,
|
||||||
|
answer_id.from_user.id,
|
||||||
|
)
|
||||||
|
await answer_kind.reply_text(
|
||||||
|
app._("remove_failure", "message", locale=user.locale).format(
|
||||||
|
answer_id.text
|
||||||
|
),
|
||||||
|
reply_markup=ReplyKeyboardRemove(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_message(~filters.scheduled & filters.command(["purge"], prefixes=["", "/"]))
|
||||||
|
async def cmd_purge(app: PyroClient, msg: Message):
|
||||||
|
if msg.from_user.id not in app.admins:
|
||||||
|
return
|
41
plugins/commands/report.py
Normal file
41
plugins/commands/report.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from libbot import sync
|
||||||
|
from pyrogram import filters
|
||||||
|
from pyrogram.client import Client
|
||||||
|
from pyrogram.types import Message, User
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_message(
|
||||||
|
~filters.scheduled
|
||||||
|
& filters.chat(sync.config_get("comments", "posting"))
|
||||||
|
& filters.reply
|
||||||
|
& filters.command(["report"], prefixes=["", "/"])
|
||||||
|
)
|
||||||
|
async def command_report(app: PyroClient, msg: Message):
|
||||||
|
if msg.reply_to_message.forward_from_chat.id != app.config["posting"]["channel"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
user = await app.find_user(msg.from_user)
|
||||||
|
|
||||||
|
await msg.reply_text(
|
||||||
|
app._(
|
||||||
|
"report_sent",
|
||||||
|
"message",
|
||||||
|
locale=user.locale if msg.from_user is not None else None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
report_sent = await msg.reply_to_message.forward(app.owner)
|
||||||
|
sender = msg.from_user if msg.from_user is not None else msg.sender_chat
|
||||||
|
|
||||||
|
sender_name = sender.first_name if isinstance(sender, User) else sender.title
|
||||||
|
|
||||||
|
await report_sent.reply_text(
|
||||||
|
app._("report_received", "message", locale=user.locale).format(
|
||||||
|
sender_name, sender.username, sender.id
|
||||||
|
),
|
||||||
|
quote=True,
|
||||||
|
)
|
312
plugins/handlers/submission.py
Normal file
312
plugins/handlers/submission.py
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from os import makedirs, path, sep
|
||||||
|
from pathlib import Path
|
||||||
|
from traceback import format_exc
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from pyrogram import filters
|
||||||
|
from pyrogram.client import Client
|
||||||
|
from pyrogram.enums.chat_action import ChatAction
|
||||||
|
from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
|
||||||
|
|
||||||
|
from classes.enums.submission_types import SubmissionType
|
||||||
|
from classes.exceptions import SubmissionDuplicatesError, SubmissionUnsupportedError
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules.database import col_submitted
|
||||||
|
from modules.utils import USERS_WITH_CONTEXT
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_message(
|
||||||
|
~filters.scheduled & filters.private & filters.photo
|
||||||
|
| filters.video
|
||||||
|
# | filters.animation
|
||||||
|
| filters.document
|
||||||
|
)
|
||||||
|
async def get_submission(app: PyroClient, msg: Message):
|
||||||
|
global USERS_WITH_CONTEXT
|
||||||
|
|
||||||
|
if not hasattr(msg.from_user, "id"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg.from_user.id in USERS_WITH_CONTEXT:
|
||||||
|
return
|
||||||
|
|
||||||
|
user = await app.find_user(msg.from_user)
|
||||||
|
user_owner = await app.find_user(app.owner)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if user.banned:
|
||||||
|
return
|
||||||
|
|
||||||
|
await app.send_chat_action(msg.chat.id, ChatAction.TYPING)
|
||||||
|
|
||||||
|
save_tmp = True
|
||||||
|
contents = None
|
||||||
|
|
||||||
|
if await user.is_limited():
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("sub_cooldown", "message", locale=user.locale).format(
|
||||||
|
app.config["submission"]["timeout"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg.document is not None:
|
||||||
|
logger.info(
|
||||||
|
"User %s is trying to submit a file of type '%s' with name '%s' and size of %s MB",
|
||||||
|
msg.from_user.id,
|
||||||
|
msg.document.mime_type,
|
||||||
|
msg.document.file_name,
|
||||||
|
msg.document.file_size / 1024 / 1024,
|
||||||
|
)
|
||||||
|
if msg.document.mime_type not in app.config["submission"]["mime_types"]:
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("mime_not_allowed", "message", locale=user.locale).format(
|
||||||
|
", ".join(app.config["submission"]["mime_types"])
|
||||||
|
),
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if msg.document.file_size > app.config["submission"]["file_size"]:
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("document_too_large", "message", locale=user.locale).format(
|
||||||
|
app.config["submission"]["file_size"] / 1024 / 1024
|
||||||
|
),
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if msg.document.file_size > app.config["submission"]["tmp_size"]:
|
||||||
|
save_tmp = False
|
||||||
|
contents = (
|
||||||
|
msg.document.file_id,
|
||||||
|
SubmissionType.DOCUMENT,
|
||||||
|
) # , msg.document.file_name
|
||||||
|
|
||||||
|
if msg.video is not None:
|
||||||
|
logger.info(
|
||||||
|
"User %s is trying to submit a video with name '%s' and size of %s MB",
|
||||||
|
msg.from_user.id,
|
||||||
|
msg.video.file_name,
|
||||||
|
msg.video.file_size / 1024 / 1024,
|
||||||
|
)
|
||||||
|
if msg.video.file_size > app.config["submission"]["file_size"]:
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("document_too_large", "message", locale=user.locale).format(
|
||||||
|
app.config["submission"]["file_size"] / 1024 / 1024
|
||||||
|
),
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if msg.video.file_size > app.config["submission"]["tmp_size"]:
|
||||||
|
save_tmp = False
|
||||||
|
contents = msg.video.file_id, SubmissionType.VIDEO # , msg.video.file_name
|
||||||
|
|
||||||
|
# if msg.animation is not None:
|
||||||
|
# logger.info(
|
||||||
|
# "User %s is trying to submit an animation with name '%s' and size of %s MB",
|
||||||
|
# msg.from_user.id,
|
||||||
|
# msg.animation.file_name,
|
||||||
|
# msg.animation.file_size / 1024 / 1024,
|
||||||
|
# )
|
||||||
|
# if msg.animation.file_size > app.config["submission"]["file_size"]:
|
||||||
|
# await msg.reply_text(
|
||||||
|
# app._("document_too_large", "message", locale=user.locale).format(
|
||||||
|
# str(app.config["submission"]["file_size"] / 1024 / 1024)
|
||||||
|
# ),
|
||||||
|
# quote=True,
|
||||||
|
# )
|
||||||
|
# return
|
||||||
|
# if msg.animation.file_size > app.config["submission"]["tmp_size"]:
|
||||||
|
# save_tmp = False
|
||||||
|
# contents = (
|
||||||
|
# msg.animation.file_id,
|
||||||
|
# SubmissionType.ANIMATION,
|
||||||
|
# ) # , msg.animation.file_name
|
||||||
|
|
||||||
|
if msg.photo is not None:
|
||||||
|
logger.info(
|
||||||
|
"User %s is trying to submit a photo with ID '%s' and size of %s MB",
|
||||||
|
msg.from_user.id,
|
||||||
|
msg.photo.file_id,
|
||||||
|
msg.photo.file_size / 1024 / 1024,
|
||||||
|
)
|
||||||
|
contents = msg.photo.file_id, SubmissionType.PHOTO # , "please_generate"
|
||||||
|
|
||||||
|
if contents is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if save_tmp is not None:
|
||||||
|
tmp_id = str(uuid4())
|
||||||
|
|
||||||
|
# filename = tmp_id if contents[1] == "please_generate" else contents[1]
|
||||||
|
makedirs(
|
||||||
|
Path(f"{app.config['locations']['data']}/submissions/{tmp_id}"),
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
downloaded = await app.download_media(
|
||||||
|
msg,
|
||||||
|
str(Path(f"{app.config['locations']['data']}/submissions/{tmp_id}"))
|
||||||
|
+ sep,
|
||||||
|
)
|
||||||
|
|
||||||
|
inserted = col_submitted.insert_one(
|
||||||
|
{
|
||||||
|
"user": msg.from_user.id,
|
||||||
|
"date": datetime.now(),
|
||||||
|
"done": False,
|
||||||
|
"type": contents[1].value,
|
||||||
|
"temp": {"uuid": tmp_id, "file": path.basename(str(downloaded))},
|
||||||
|
"telegram": {"msg_id": msg.id, "file_id": contents[0]},
|
||||||
|
"caption": str(msg.caption) if msg.caption is not None else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
inserted = col_submitted.insert_one(
|
||||||
|
{
|
||||||
|
"user": msg.from_user.id,
|
||||||
|
"date": datetime.now(),
|
||||||
|
"done": False,
|
||||||
|
"type": contents[1].value,
|
||||||
|
"temp": {"uuid": None, "file": None},
|
||||||
|
"telegram": {"msg_id": msg.id, "file_id": contents[0]},
|
||||||
|
"caption": str(msg.caption) if msg.caption is not None else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
buttons = [
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=app._("sub_yes", "button", locale=user_owner.locale),
|
||||||
|
callback_data=f"sub_yes_{str(inserted.inserted_id)}",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
if msg.caption is not None:
|
||||||
|
caption = str(msg.caption)
|
||||||
|
buttons[0].append(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=app._("sub_yes_caption", "button", locale=user_owner.locale),
|
||||||
|
callback_data=f"sub_yes_{str(inserted.inserted_id)}_caption",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
caption = ""
|
||||||
|
|
||||||
|
buttons[0].append(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=app._("sub_no", "button", locale=user_owner.locale),
|
||||||
|
callback_data=f"sub_no_{str(inserted.inserted_id)}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
caption += app._("sub_by", "message", locale=user_owner.locale)
|
||||||
|
|
||||||
|
if msg.from_user.first_name is not None:
|
||||||
|
caption += f" {msg.from_user.first_name}"
|
||||||
|
if msg.from_user.last_name is not None:
|
||||||
|
caption += f" {msg.from_user.last_name}"
|
||||||
|
if msg.from_user.username is not None:
|
||||||
|
caption += f" (@{msg.from_user.username})"
|
||||||
|
if msg.from_user.phone_number is not None:
|
||||||
|
caption += f" ({msg.from_user.phone_number})"
|
||||||
|
|
||||||
|
if (
|
||||||
|
msg.from_user.id in app.admins
|
||||||
|
and app.config["submission"]["require_confirmation"]["admins"] is False
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
submitted = await app.submit_media(str(inserted.inserted_id))
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("sub_yes_auto", "message", locale=user.locale),
|
||||||
|
disable_notification=True,
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
if app.config["submission"]["send_uploaded_id"]:
|
||||||
|
caption += f"\n\nID: `{submitted[1]}`"
|
||||||
|
await msg.copy(app.owner, caption=caption, disable_notification=True)
|
||||||
|
return
|
||||||
|
except SubmissionUnsupportedError:
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("mime_not_allowed", "message", locale=user.locale).format(
|
||||||
|
", ".join(app.config["submission"]["mime_types"]), quote=True
|
||||||
|
),
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except SubmissionDuplicatesError as exp:
|
||||||
|
await msg.reply_text(
|
||||||
|
app._(
|
||||||
|
"sub_media_duplicates_list", "message", locale=user.locale
|
||||||
|
).format("\n • ".join(exp.duplicates)),
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception as exp:
|
||||||
|
await msg.reply_text(format_exc(), quote=True)
|
||||||
|
return
|
||||||
|
elif (
|
||||||
|
msg.from_user.id not in app.admins
|
||||||
|
and app.config["submission"]["require_confirmation"]["users"] is False
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
submitted = await app.submit_photo(str(inserted.inserted_id))
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("sub_yes_auto", "message", locale=user.locale),
|
||||||
|
disable_notification=True,
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
if app.config["submission"]["send_uploaded_id"]:
|
||||||
|
caption += f"\n\nID: `{submitted[1]}`"
|
||||||
|
await msg.copy(app.owner, caption=caption)
|
||||||
|
return
|
||||||
|
except SubmissionUnsupportedError:
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("mime_not_allowed", "message", locale=user.locale).format(
|
||||||
|
", ".join(app.config["submission"]["mime_types"]), quote=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except SubmissionDuplicatesError as exp:
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("sub_dup", "message", locale=user.locale), quote=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception as exp:
|
||||||
|
await app.send_message(
|
||||||
|
app.owner,
|
||||||
|
app._(
|
||||||
|
"sub_error_admin", "message", locale=user_owner.locale
|
||||||
|
).format(msg.from_user.id, format_exc()),
|
||||||
|
)
|
||||||
|
await msg.reply_text("sub_error", quote=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg.from_user.id not in app.admins:
|
||||||
|
buttons += [
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=app._("sub_block", "button", locale=user_owner.locale),
|
||||||
|
callback_data=f"sub_block_{msg.from_user.id}",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
await user.update_cooldown()
|
||||||
|
|
||||||
|
if msg.from_user.id != app.owner:
|
||||||
|
await msg.reply_text(
|
||||||
|
app._("sub_sent", "message", locale=user.locale),
|
||||||
|
disable_notification=True,
|
||||||
|
quote=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
await msg.copy(
|
||||||
|
app.owner, caption=caption, reply_markup=InlineKeyboardMarkup(buttons)
|
||||||
|
)
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
logger.error("'from_user' does not seem to contain 'id'")
|
42
plugins/language.py
Normal file
42
plugins/language.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from pykeyboard import InlineButton, InlineKeyboard
|
||||||
|
from pyrogram import filters
|
||||||
|
from pyrogram.client import Client
|
||||||
|
from pyrogram.types import CallbackQuery, Message
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_message(
|
||||||
|
~filters.scheduled & filters.private & filters.command(["language"], prefixes=["/"]) # type: ignore
|
||||||
|
)
|
||||||
|
async def command_language(app: PyroClient, message: Message):
|
||||||
|
user = await app.find_user(message.from_user)
|
||||||
|
keyboard = InlineKeyboard(row_width=2)
|
||||||
|
buttons = []
|
||||||
|
|
||||||
|
for locale, data in app.in_every_locale("metadata").items():
|
||||||
|
buttons.append(
|
||||||
|
InlineButton(f"{data['flag']} {data['name']}", f"language:{locale}")
|
||||||
|
)
|
||||||
|
|
||||||
|
keyboard.add(*buttons)
|
||||||
|
|
||||||
|
await message.reply_text(
|
||||||
|
app._("locale_choice", "message", locale=user.locale),
|
||||||
|
reply_markup=keyboard,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_callback_query(filters.regex(r"language:[\s\S]*")) # type: ignore
|
||||||
|
async def callback_language(app: PyroClient, callback: CallbackQuery):
|
||||||
|
user = await app.find_user(callback.from_user)
|
||||||
|
language = str(callback.data).split(":")[1]
|
||||||
|
|
||||||
|
await user.update_locale(language)
|
||||||
|
|
||||||
|
await callback.answer(
|
||||||
|
app._("locale_set", "callback", locale=language).format(
|
||||||
|
locale=app._("name", "metadata", locale=language)
|
||||||
|
),
|
||||||
|
show_alert=True,
|
||||||
|
)
|
13
plugins/remove_commands.py
Normal file
13
plugins/remove_commands.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from pyrogram import filters
|
||||||
|
from pyrogram.client import Client
|
||||||
|
from pyrogram.types import Message
|
||||||
|
|
||||||
|
from classes.pyroclient import PyroClient
|
||||||
|
|
||||||
|
|
||||||
|
@Client.on_message(
|
||||||
|
~filters.scheduled & filters.private & filters.command(["remove_commands"], prefixes=["/"]) # type: ignore
|
||||||
|
)
|
||||||
|
async def command_remove_commands(app: PyroClient, msg: Message):
|
||||||
|
await msg.reply_text("Okay.")
|
||||||
|
await app.remove_commands(command_sets=await app.collect_commands())
|
461
poster.py
461
poster.py
@@ -1,461 +0,0 @@
|
|||||||
from os import sep, remove, getpid, listdir
|
|
||||||
from random import choice
|
|
||||||
from shutil import move
|
|
||||||
from sys import argv, exit
|
|
||||||
from threading import Thread
|
|
||||||
from time import time
|
|
||||||
from traceback import format_exc
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from modules.logging import logWrite
|
|
||||||
from modules.utils import configGet, jsonLoad, jsonSave, killProc, locale
|
|
||||||
|
|
||||||
# Args =====================================================================================================================================
|
|
||||||
if "--move-sent" in argv:
|
|
||||||
for entry in jsonLoad(configGet("index", "locations"))["sent"]:
|
|
||||||
try:
|
|
||||||
move(configGet("queue", "locations")+sep+entry, configGet("sent", "locations")+sep+entry)
|
|
||||||
except FileNotFoundError:
|
|
||||||
logWrite(locale("move_sent_doesnt_exist", "console", locale=configGet("locale")).format(entry))
|
|
||||||
except Exception as exp:
|
|
||||||
logWrite(locale("move_sent_doesnt_exception", "console", locale=configGet("locale")).format(entry, exp))
|
|
||||||
logWrite(locale("move_sent_completed", "console", locale=configGet("locale")))
|
|
||||||
|
|
||||||
if "--cleanup" in argv:
|
|
||||||
if "--confirm" in argv:
|
|
||||||
index = jsonLoad(configGet("index", "locations"))
|
|
||||||
for entry in index["sent"]:
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
remove(configGet("queue", "locations")+sep+entry)
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
remove(configGet("sent", "locations")+sep+entry)
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
except Exception as exp:
|
|
||||||
logWrite(locale("cleanup_exception", "console", locale=configGet("locale")).format(entry, exp))
|
|
||||||
jsonSave(index, jsonLoad(configGet("index", "locations")))
|
|
||||||
logWrite(locale("cleanup_completed", "console", locale=configGet("locale")))
|
|
||||||
else:
|
|
||||||
logWrite(locale("cleanup_unathorized", "console", locale=configGet("locale")))
|
|
||||||
|
|
||||||
if "--cleanup-index" in argv:
|
|
||||||
if "--confirm" in argv:
|
|
||||||
index = jsonLoad(configGet("index", "locations"))
|
|
||||||
index["sent"] = []
|
|
||||||
jsonSave(index, jsonLoad(configGet("index", "locations")))
|
|
||||||
logWrite(locale("cleanup_index_completed", "console", locale=configGet("locale")))
|
|
||||||
else:
|
|
||||||
logWrite(locale("cleanup_index_unathorized", "console", locale=configGet("locale")))
|
|
||||||
|
|
||||||
if "--norun" in argv:
|
|
||||||
logWrite(locale("passed_norun", "console", locale=configGet("locale")))
|
|
||||||
exit()
|
|
||||||
#===========================================================================================================================================
|
|
||||||
|
|
||||||
|
|
||||||
# Import ===================================================================================================================================
|
|
||||||
try:
|
|
||||||
import schedule # type: ignore
|
|
||||||
from pyrogram import Client, filters, idle # type: ignore
|
|
||||||
from pyrogram.types import ChatPermissions, ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton, BotCommand, BotCommandScopeChat # type: ignore
|
|
||||||
from pyrogram.raw.types import UpdateChannelMessageForwards, InputChannel, InputPeerChannel
|
|
||||||
from pyrogram.raw.functions.stats import GetMessagePublicForwards
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
print(locale("deps_missing", "console", locale=configGet("locale")), flush=True)
|
|
||||||
exit()
|
|
||||||
#===========================================================================================================================================
|
|
||||||
|
|
||||||
|
|
||||||
pid = getpid()
|
|
||||||
app = Client("duptsiaposter", bot_token=configGet("bot_token", "bot"), api_id=configGet("api_id", "bot"), api_hash=configGet("api_hash", "bot"))
|
|
||||||
|
|
||||||
|
|
||||||
# Work in progress
|
|
||||||
# def check_forwards(app):
|
|
||||||
|
|
||||||
# try:
|
|
||||||
|
|
||||||
# index = jsonLoad(configGet("index", "locations"))
|
|
||||||
# channel = app.get_chat(configGet("channel", "posting"))
|
|
||||||
|
|
||||||
# peer = app.resolve_peer(configGet("channel", "posting"))
|
|
||||||
# print(peer, flush=True)
|
|
||||||
|
|
||||||
# posts_list = [i for i in range(index["last_id"]-100,index["last_id"])]
|
|
||||||
# last_posts = app.get_messages(configGet("channel", "posting"), message_ids=posts_list)
|
|
||||||
|
|
||||||
# for post in last_posts:
|
|
||||||
# post_forwards = GetMessagePublicForwards(channel=peer, msg_id=post.id, offset_peer=peer, offset_rate=0, offset_id=0, limit=100)
|
|
||||||
# print(post_forwards, flush=True)
|
|
||||||
# for forward in post_forwards:
|
|
||||||
# print(forward, flush=True)
|
|
||||||
|
|
||||||
# except Exception as exp:
|
|
||||||
|
|
||||||
# logWrite("Could not get last posts forwards due to {0} with traceback {1}".format(str(exp), traceback.format_exc()), debug=True)
|
|
||||||
|
|
||||||
# if configGet("error", "reports"):
|
|
||||||
# app.send_message(configGet("admin"), traceback.format_exc()) # type: ignore
|
|
||||||
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
def send_content():
|
|
||||||
|
|
||||||
# Send post to channel
|
|
||||||
try:
|
|
||||||
|
|
||||||
index = jsonLoad(configGet("index", "locations"))
|
|
||||||
list_queue = listdir(configGet("queue", "locations"))
|
|
||||||
|
|
||||||
for file in list_queue:
|
|
||||||
|
|
||||||
if not file in index["sent"]:
|
|
||||||
|
|
||||||
ext_match = False
|
|
||||||
|
|
||||||
for ext in configGet("photo", "posting", "extensions"):
|
|
||||||
if file.endswith(ext):
|
|
||||||
ext_match = True
|
|
||||||
ext_type = "photo"
|
|
||||||
break
|
|
||||||
|
|
||||||
for ext in configGet("video", "posting", "extensions"):
|
|
||||||
if file.endswith(ext):
|
|
||||||
ext_match = True
|
|
||||||
ext_type = "video"
|
|
||||||
break
|
|
||||||
|
|
||||||
if not ext_match:
|
|
||||||
list_queue.remove(file)
|
|
||||||
|
|
||||||
else:
|
|
||||||
list_queue.remove(file)
|
|
||||||
|
|
||||||
if len(list_queue) > 0:
|
|
||||||
candidate_file = choice(list_queue)
|
|
||||||
candidate = configGet("queue", "locations")+sep+candidate_file
|
|
||||||
else:
|
|
||||||
logWrite(locale("post_empty", "console", locale=configGet("locale")))
|
|
||||||
if configGet("error", "reports"):
|
|
||||||
app.send_message(configGet("admin"), locale("post_empty", "message", locale=configGet("locale"))) # type: ignore
|
|
||||||
return
|
|
||||||
|
|
||||||
if candidate_file in index["captions"]:
|
|
||||||
caption = index["captions"][candidate_file]
|
|
||||||
else:
|
|
||||||
caption = ""
|
|
||||||
|
|
||||||
if configGet("enabled", "caption"):
|
|
||||||
if configGet("link", "caption") != None:
|
|
||||||
caption = f"{caption}\n\n[{configGet('text', 'caption')}]({configGet('link', 'caption')})"
|
|
||||||
else:
|
|
||||||
caption = f"{caption}\n\n{configGet('text', 'caption')}"
|
|
||||||
else:
|
|
||||||
caption = caption
|
|
||||||
|
|
||||||
if ext_type == "photo": # type: ignore
|
|
||||||
|
|
||||||
if configGet("enabled", "caption"):
|
|
||||||
if configGet("link", "caption") != None:
|
|
||||||
sent = app.send_photo(configGet("channel", "posting"), candidate, caption=caption, disable_notification=configGet("silent", "posting")) # type: ignore
|
|
||||||
else:
|
|
||||||
sent = app.send_photo(configGet("channel", "posting"), candidate, caption=caption, disable_notification=configGet("silent", "posting")) # type: ignore
|
|
||||||
else:
|
|
||||||
sent = app.send_photo(configGet("channel", "posting"), candidate, caption=caption, disable_notification=configGet("silent", "posting")) # type: ignore
|
|
||||||
|
|
||||||
elif ext_type == "video": # type: ignore
|
|
||||||
|
|
||||||
if configGet("enabled", "caption"):
|
|
||||||
if configGet("link", "caption") != None:
|
|
||||||
sent = app.send_video(configGet("channel", "posting"), candidate, caption=caption, disable_notification=configGet("silent", "posting")) # type: ignore
|
|
||||||
else:
|
|
||||||
sent = app.send_video(configGet("channel", "posting"), candidate, caption=caption, disable_notification=configGet("silent", "posting")) # type: ignore
|
|
||||||
else:
|
|
||||||
sent = app.send_video(configGet("channel", "posting"), candidate, caption=caption, disable_notification=configGet("silent", "posting")) # type: ignore
|
|
||||||
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
index["sent"].append(candidate_file)
|
|
||||||
index["last_id"] = sent.id
|
|
||||||
|
|
||||||
jsonSave(index, configGet("index", "locations"))
|
|
||||||
|
|
||||||
if configGet("move_sent", "posting"):
|
|
||||||
move(candidate, configGet("sent", "locations")+sep+candidate_file)
|
|
||||||
|
|
||||||
logWrite(locale("post_sent", "console", locale=configGet("locale")).format(candidate, ext_type, str(configGet("channel", "posting")), caption.replace("\n", "%n"), str(configGet("silent", "posting")))) # type: ignore
|
|
||||||
|
|
||||||
if configGet("sent", "reports"):
|
|
||||||
app.send_message(configGet("admin"), f"Posted `{candidate_file}`", disable_web_page_preview=True, reply_markup=InlineKeyboardMarkup([
|
|
||||||
[InlineKeyboardButton(locale("post_view", "button", locale=configGet("locale")), url=sent.link)] # type: ignore
|
|
||||||
])) # type: ignore
|
|
||||||
|
|
||||||
except Exception as exp:
|
|
||||||
logWrite(locale("post_exception", "console", locale=configGet("locale")).format(str(exp), format_exc()))
|
|
||||||
if configGet("error", "reports"):
|
|
||||||
app.send_message(configGet("admin"), locale("post_exception", "message", locale=configGet("locale")).format(exp, traceback.format_exc())) # type: ignore
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# Work in progress
|
|
||||||
# Check last posts forwards
|
|
||||||
# check_forwards(app)
|
|
||||||
|
|
||||||
if configGet("submit", "mode"):
|
|
||||||
@app.on_message(~ filters.scheduled & filters.command(["start"], prefixes="/"))
|
|
||||||
def cmd_start(app, msg):
|
|
||||||
if msg.from_user.id not in jsonLoad(configGet("blocked", "locations")):
|
|
||||||
msg.reply_text(locale("start", "message", locale=msg.from_user.language_code))
|
|
||||||
|
|
||||||
if configGet("submit", "mode"):
|
|
||||||
@app.on_message(~ filters.scheduled & filters.command(["rules", "help"], prefixes="/"))
|
|
||||||
def cmd_rules(app, msg):
|
|
||||||
if msg.from_user.id not in jsonLoad(configGet("blocked", "locations")):
|
|
||||||
msg.reply_text(locale("rules", "message", locale=msg.from_user.language_code))
|
|
||||||
|
|
||||||
# Work in progress
|
|
||||||
# @app.on_message(~ filters.scheduled & filters.command(["forwards"], prefixes="/"))
|
|
||||||
# def cmd_forwards(app, msg):
|
|
||||||
# check_forwards(app)
|
|
||||||
|
|
||||||
@app.on_message(~ filters.scheduled & filters.command(["kill", "die", "reboot"], prefixes=["", "/"]))
|
|
||||||
def cmd_kill(app, msg):
|
|
||||||
|
|
||||||
if msg.from_user.id == configGet("admin"):
|
|
||||||
logWrite(locale("shutdown", "console", locale=configGet("locale")).format(str(pid)))
|
|
||||||
msg.reply_text(locale("shutdown", "message", locale=configGet("locale")).format(str(pid)))
|
|
||||||
killProc(pid)
|
|
||||||
|
|
||||||
|
|
||||||
# Submission =====================================================================================================================================
|
|
||||||
def subLimit(user):
|
|
||||||
submit = jsonLoad(configGet("submit", "locations"))
|
|
||||||
submit[str(user.id)] = time()
|
|
||||||
jsonSave(submit, configGet("submit", "locations"))
|
|
||||||
|
|
||||||
def subLimited(user):
|
|
||||||
if user.id == configGet("admin"):
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
submit = jsonLoad(configGet("submit", "locations"))
|
|
||||||
if str(user.id) in submit:
|
|
||||||
if (time() - submit[str(user.id)]) < configGet("timeout", "submission"):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def subBlock(user):
|
|
||||||
blocked = jsonLoad(configGet("blocked", "locations"))
|
|
||||||
if user not in blocked:
|
|
||||||
blocked.append(user)
|
|
||||||
jsonSave(blocked, configGet("blocked", "locations"))
|
|
||||||
|
|
||||||
def subUnblock(user):
|
|
||||||
blocked = jsonLoad(configGet("blocked", "locations"))
|
|
||||||
if user in blocked:
|
|
||||||
blocked.remove(user)
|
|
||||||
jsonSave(blocked, configGet("blocked", "locations"))
|
|
||||||
|
|
||||||
if configGet("submit", "mode"):
|
|
||||||
@app.on_message(~ filters.scheduled & filters.photo | filters.video | filters.animation | filters.document)
|
|
||||||
def get_submission(_, msg):
|
|
||||||
try:
|
|
||||||
if msg.from_user.id not in jsonLoad(configGet("blocked", "locations")):
|
|
||||||
user_locale = msg.from_user.language_code
|
|
||||||
if not subLimited(msg.from_user):
|
|
||||||
|
|
||||||
if msg.document != None:
|
|
||||||
if msg.document.mime_type not in configGet("mime_types", "submission"):
|
|
||||||
msg.reply_text(locale("mime_not_allowed", "message", locale=user_locale), quote=True)
|
|
||||||
return
|
|
||||||
if msg.document.file_size > configGet("file_size", "submission"):
|
|
||||||
msg.reply_text(locale("document_too_large", "message", locale=user_locale).format(str(configGet("file_size", "submission")/1024/1024)), quote=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
if msg.video != None:
|
|
||||||
if msg.video.file_size > configGet("file_size", "submission"):
|
|
||||||
msg.reply_text(locale("document_too_large", "message", locale=user_locale).format(str(configGet("file_size", "submission")/1024/1024)), quote=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
buttons = [
|
|
||||||
[
|
|
||||||
InlineKeyboardButton(text=locale("sub_yes", "button", locale=configGet("locale")), callback_data=f"sub_yes_{msg.from_user.id}_{msg.id}")
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
if msg.caption != None:
|
|
||||||
caption = str(msg.caption)
|
|
||||||
buttons[0].append(
|
|
||||||
InlineKeyboardButton(text=locale("sub_yes_caption", "button", locale=configGet("locale")), callback_data=f"sub_yes_{msg.from_user.id}_{msg.id}_caption")
|
|
||||||
)
|
|
||||||
buttons[0].append(
|
|
||||||
InlineKeyboardButton(text=locale("sub_no", "button", locale=configGet("locale")), callback_data=f"sub_no_{msg.from_user.id}_{msg.id}")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
caption = ""
|
|
||||||
buttons[0].append(
|
|
||||||
InlineKeyboardButton(text=locale("sub_no", "button", locale=configGet("locale")), callback_data=f"sub_no_{msg.from_user.id}_{msg.id}")
|
|
||||||
)
|
|
||||||
|
|
||||||
caption += locale("sub_by", "message", locale=locale(configGet("locale")))
|
|
||||||
|
|
||||||
if msg.from_user.first_name != None:
|
|
||||||
caption += f" {msg.from_user.first_name}"
|
|
||||||
if msg.from_user.last_name != None:
|
|
||||||
caption += f" {msg.from_user.last_name}"
|
|
||||||
if msg.from_user.username != None:
|
|
||||||
caption += f" (@{msg.from_user.username})"
|
|
||||||
if msg.from_user.phone_number != None:
|
|
||||||
caption += f" ({msg.from_user.phone_number})"
|
|
||||||
|
|
||||||
if msg.from_user.id != configGet("admin"):
|
|
||||||
buttons += [
|
|
||||||
[
|
|
||||||
InlineKeyboardButton(text=locale("sub_block", "button", locale=configGet("locale")), callback_data=f"sub_block_{msg.from_user.id}")
|
|
||||||
],
|
|
||||||
[
|
|
||||||
InlineKeyboardButton(text=locale("sub_unblock", "button", locale=configGet("locale")), callback_data=f"sub_unblock_{msg.from_user.id}")
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
msg.reply_text(locale("sub_sent", "message", locale=user_locale), quote=True)
|
|
||||||
subLimit(msg.from_user)
|
|
||||||
|
|
||||||
msg.copy(configGet("admin"), caption=caption, reply_markup=InlineKeyboardMarkup(buttons))
|
|
||||||
|
|
||||||
else:
|
|
||||||
msg.reply_text(locale("sub_cooldown", "message", locale=user_locale).format(str(configGet("timeout", "submission"))))
|
|
||||||
except AttributeError:
|
|
||||||
logWrite(f"from_user in function get_submission does not seem to contain id")
|
|
||||||
|
|
||||||
@app.on_callback_query(filters.regex("sub_yes_[\s\S]*_[\s\S]*")) # type: ignore
|
|
||||||
def callback_query_yes(app, clb): # type: ignore
|
|
||||||
fullclb = clb.data.split("_")
|
|
||||||
user_locale = clb.from_user.language_code
|
|
||||||
try:
|
|
||||||
submission = app.get_messages(int(fullclb[2]), int(fullclb[3]))
|
|
||||||
except:
|
|
||||||
clb.answer(text=locale("sub_msg_unavail", "message", locale=user_locale), show_alert=True)
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
media = app.download_media(submission, file_name=configGet("queue", "locations")+sep)
|
|
||||||
if clb.data.endswith("_caption"):
|
|
||||||
index = jsonLoad(configGet("index", "locations"))
|
|
||||||
index["captions"][Path(media).name] = submission.caption
|
|
||||||
jsonSave(index, configGet("index", "locations"))
|
|
||||||
except:
|
|
||||||
clb.answer(text=locale("sub_media_unavail", "message", locale=user_locale), show_alert=True)
|
|
||||||
return
|
|
||||||
submission.reply_text(locale("sub_yes", "message", locale=submission.from_user.language_code), quote=True)
|
|
||||||
clb.answer(text=locale("sub_yes", "callback", locale=user_locale).format(fullclb[2]), show_alert=True)
|
|
||||||
|
|
||||||
@app.on_callback_query(filters.regex("sub_no_[\s\S]*_[\s\S]*")) # type: ignore
|
|
||||||
def callback_query_no(app, clb): # type: ignore
|
|
||||||
fullclb = clb.data.split("_")
|
|
||||||
user_locale = clb.from_user.language_code
|
|
||||||
try:
|
|
||||||
submission = app.get_messages(int(fullclb[2]), int(fullclb[3]))
|
|
||||||
except:
|
|
||||||
clb.answer(text=locale("sub_msg_unavail", "message", locale=user_locale), show_alert=True)
|
|
||||||
return
|
|
||||||
submission.reply_text(locale("sub_no", "message", locale=submission.from_user.language_code), quote=True)
|
|
||||||
clb.answer(text=locale("sub_no", "callback", locale=user_locale).format(fullclb[2]), show_alert=True)
|
|
||||||
|
|
||||||
@app.on_callback_query(filters.regex("sub_block_[\s\S]*")) # type: ignore
|
|
||||||
def callback_query_block(app, clb): # type: ignore
|
|
||||||
fullclb = clb.data.split("_")
|
|
||||||
user_locale = clb.from_user.language_code
|
|
||||||
app.send_message(int(fullclb[2]), locale("sub_msg_unavail", "message", locale=configGet("locale")))
|
|
||||||
subBlock(int(fullclb[2]))
|
|
||||||
clb.answer(text=locale("sub_block", "callback", locale=user_locale).format(fullclb[2]), show_alert=True)
|
|
||||||
|
|
||||||
@app.on_callback_query(filters.regex("sub_unblock_[\s\S]*")) # type: ignore
|
|
||||||
def callback_query_unblock(app, clb): # type: ignore
|
|
||||||
fullclb = clb.data.split("_")
|
|
||||||
user_locale = clb.from_user.language_code
|
|
||||||
app.send_message(int(fullclb[2]), locale("sub_msg_unavail", "message", locale=configGet("locale")))
|
|
||||||
subUnblock(int(fullclb[2]))
|
|
||||||
clb.answer(text=locale("sub_unblock", "callback", locale=user_locale).format(fullclb[2]), show_alert=True)
|
|
||||||
#===========================================================================================================================================
|
|
||||||
|
|
||||||
# Work in progress
|
|
||||||
# Handle new forwards
|
|
||||||
# @app.on_raw_update()
|
|
||||||
# def fwd_got(app, update, users, chats):
|
|
||||||
# if isinstance(update, UpdateChannelMessageForwards):
|
|
||||||
# logWrite(f'Forward count increased to {update["forwards"]} on post {update["id"]} in channel {update["channel_id"]}')
|
|
||||||
# logWrite(str(users), debug=True)
|
|
||||||
# logWrite(str(chats), debug=True)
|
|
||||||
# # else:
|
|
||||||
# # logWrite(f"Got raw update of type {type(update)} with contents {update}", debug=True)
|
|
||||||
|
|
||||||
if configGet("post", "mode"):
|
|
||||||
|
|
||||||
for entry in configGet("time", "posting"):
|
|
||||||
schedule.every().day.at(entry).do(send_content)
|
|
||||||
|
|
||||||
def background_task():
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
schedule.run_pending()
|
|
||||||
time.sleep(1)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
except Exception as exp:
|
|
||||||
logWrite(locale("exception_occured", "console", locale=configGet("locale")).format(exp))
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
logWrite(locale("keyboard_interrupt", "console", locale=configGet("locale")))
|
|
||||||
if configGet("shutdown", "reports"):
|
|
||||||
app.send_message(configGet("admin"), locale("shutdown", "message", locale=configGet("locale")).format(str(pid))) # type: ignore
|
|
||||||
killProc(pid)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
|
|
||||||
logWrite(locale("startup", "console", locale=configGet("locale")).format(str(pid)))
|
|
||||||
|
|
||||||
app.start() # type: ignore
|
|
||||||
if configGet("startup", "reports"):
|
|
||||||
app.send_message(configGet("admin"), locale("startup", "message", locale=configGet("locale")).format(str(pid))) # type: ignore
|
|
||||||
|
|
||||||
if configGet("post", "mode"):
|
|
||||||
t = Thread(target=background_task)
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
if configGet("submit", "mode"):
|
|
||||||
# Registering user commands
|
|
||||||
for entry in listdir(configGet("locale", "locations")):
|
|
||||||
if entry.endswith(".json"):
|
|
||||||
commands_list = []
|
|
||||||
for command in configGet("commands"):
|
|
||||||
commands_list.append(BotCommand(command, locale(command, "commands", locale=entry.replace(".json", ""))))
|
|
||||||
app.set_bot_commands(commands_list, language_code=entry.replace(".json", "")) # type: ignore
|
|
||||||
|
|
||||||
# Registering user commands for fallback locale
|
|
||||||
commands_list = []
|
|
||||||
for command in configGet("commands"):
|
|
||||||
commands_list.append(BotCommand(command, locale(command, "commands", locale=configGet("locale_fallback"))))
|
|
||||||
app.set_bot_commands(commands_list) # type: ignore
|
|
||||||
|
|
||||||
# Registering admin commands
|
|
||||||
commands_admin_list = []
|
|
||||||
if configGet("submit", "mode"):
|
|
||||||
for command in configGet("commands"):
|
|
||||||
commands_admin_list.append(BotCommand(command, locale(command, "commands", locale=configGet("locale"))))
|
|
||||||
for command in configGet("commands_admin"):
|
|
||||||
commands_admin_list.append(BotCommand(command, locale(command, "commands_admin", locale=configGet("locale"))))
|
|
||||||
app.set_bot_commands(commands_admin_list, scope=BotCommandScopeChat(chat_id=configGet("admin"))) # type: ignore
|
|
||||||
|
|
||||||
idle()
|
|
||||||
|
|
||||||
app.send_message(configGet("admin"), locale("shutdown", "message", locale=configGet("locale")).format(str(pid))) # type: ignore
|
|
||||||
logWrite(locale("shutdown", "console", locale=configGet("locale")).format(str(pid)))
|
|
||||||
|
|
||||||
killProc(pid)
|
|
@@ -1 +0,0 @@
|
|||||||
ujson
|
|
@@ -1,3 +1,15 @@
|
|||||||
schedule
|
aiohttp~=3.8.4
|
||||||
pyrogram>=2.0.0
|
black~=23.3.0
|
||||||
psutil
|
convopyro==0.5
|
||||||
|
pillow~=10.0.0
|
||||||
|
psutil~=5.9.4
|
||||||
|
pykeyboard==0.1.5
|
||||||
|
pymongo~=4.4.0
|
||||||
|
pyrogram==2.0.106
|
||||||
|
python_dateutil==2.8.2
|
||||||
|
pytimeparse~=1.1.8
|
||||||
|
tgcrypto==1.2.5
|
||||||
|
uvloop==0.17.0
|
||||||
|
--extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple
|
||||||
|
libbot[speed,pyrogram]==1.8
|
||||||
|
photosapi_client==0.5.0
|
@@ -3,4 +3,4 @@
|
|||||||
REM You can cd to your directory here, if you want
|
REM You can cd to your directory here, if you want
|
||||||
REM cd C:\Users\user\TelegramPoster
|
REM cd C:\Users\user\TelegramPoster
|
||||||
|
|
||||||
python poster.py
|
python main.py
|
Reference in New Issue
Block a user