16 Commits

30 changed files with 661 additions and 953 deletions

4
.gitignore vendored
View File

@@ -153,12 +153,8 @@ cython_debug/
#.idea/ #.idea/
# Custom # Custom
cache/
config.json config.json
*.session *.session
*.session-wal
*.session-shm
*.session-journal *.session-journal
venv venv

View File

@@ -1,17 +1,17 @@
<h1 align="center">TelegramPoster</h1> <h1 align="center">TelegramPoster</h1>
<p align="center"> <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/src/branch/dev/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> <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> </p>
> Шукаєш інструкцію українською? А вона [ось тут](https://git.end-play.xyz/profitroll/TelegramPoster/src/branch/master/README_uk.md) знаходиться) > Шукаєш інструкцію українською? А вона [ось тут](https://git.end-play.xyz/profitroll/TelegramPoster/src/branch/dev/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 ## Dependencies
* [Python 3.8+](https://www.python.org) (3.9+ recommended) * [Python 3.7+](https://www.python.org) (3.9+ recommended)
* [MongoDB](https://www.mongodb.com) * [MongoDB](https://www.mongodb.com)
* [PhotosAPI](https://git.end-play.xyz/profitroll/PhotosAPI) * [PhotosAPI](https://git.end-play.xyz/profitroll/PhotosAPI)
@@ -21,7 +21,7 @@ Please note that Photos API also requires MongoDB so it makes sense to install a
## Installation ## Installation
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. To make this bot run at first you need to have a Python interpreter, Photos API, MongoDB and optionally git. 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
@@ -29,7 +29,7 @@ To make this bot run at first you need to have a Python interpreter, Photos API,
> If it's non-standard 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. Install MongoDB and Photos API: 1. Install Mongo and Photos API:
1. Install MongoDB by following [official installation manual](https://www.mongodb.com/docs/manual/installation) 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. Install Photos API by following [Photos API's README](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md)
@@ -42,19 +42,19 @@ To make this bot run at first you need to have a Python interpreter, Photos API,
3. Create virtual environment [Optional]: 3. Create virtual environment [Optional]:
1. Install virtualenv module: `pip install virtualenv` 1. Install virtualenv module: `pip install virtualenv`
2. Create venv: `python -m venv .venv` 2. Create venv: `python -m venv env`
3. Activate it using `source .venv/bin/activate` on Linux, `.venv\Scripts\activate.bat` in CMD or `.venv\Scripts\Activate.ps1` in PowerShell. 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: 4. Install project's 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.
5. Configure required keys with your favorite text editor: 5. Configure "bot" and "owner" with your favorite text editor:
1. Copy config file: `cp config_example.json config.json` 1. Copy file `config_example.json` to `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 2. Open `config.json` using your favorite text editor. For example `nano config.json`, but 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. 3. Change `"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). 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).
@@ -66,26 +66,26 @@ To make this bot run at first you need to have a Python interpreter, Photos API,
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). 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: 2. Configure Photos API:
1. Change `"posting.api.address"` and `"posting.api.address_external"` to the ones your API server uses 1. Change `"posting.api.address"` to the one your API servers 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. 2. Run your bot using `python poster.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: 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. 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. After that simply set `"posting.channel"` to your channel's ID.
8. Configure posting time: 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`. 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! 9. Good to go, run it!
Make sure MongoDB and Photos API are running and use `python main.py` to start the bot. Make sure MongoDB and Photos API are running and use `python poster.py` to start it.
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 `/shutdown` 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. 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.
@@ -94,8 +94,8 @@ Of course bot also has them. You can perform some actions with them.
Examples: Examples:
* `python main.py --create-user` * `python poster.py --create-user`
* `python main.py --create-user --create-album` * `python poster.py --create-user --create-album`
## Tips and improvements ## Tips and improvements
@@ -105,6 +105,6 @@ Examples:
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`. 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`. 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 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. For console output and logging locale you should edit `"locale_log"`.
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.

View File

@@ -5,104 +5,67 @@
<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> <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> </p>
Цей бот використовується для однієї-єдиної задачі - публікувати фотографії з мого особистого архіву. Ось його код, тож ви також можете запустити бота і погратися з ним самостійно. Тільки не очікуйте, що він буде ідеальним. Це не так. Але ви завжди можете його форкнути ;) ## ⚠️ Українська версія README dev гілки ще не готова! Користуйтесь англійською! ⚠️
## Залежності Цей бот використовується для однієї-єдиної задачі - розміщувати фотографії з мого особистого архіву. Ось його код, тож Ви також можете захостити бота самостійно та розважитися з ним. Тільки не очікуйте, що він ідеальним. Не буде. Але гей, Ви завжди можете його доробити під себе ;)
* [Python 3.8+](https://www.python.org) (рекомендується 3.9+) ## Установка
* [MongoDB](https://www.mongodb.com)
* [PhotosAPI](https://git.end-play.xyz/profitroll/PhotosAPI)
Користуйтесь [інструкцією зі встановлення MongoDB](https://www.mongodb.com/docs/manual/installation) та [README Photos API](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md). Для запуску цього бота спочатку потрібно мати інтерпретатор Python та встановлений git. Google — Ваш друг у пошуках. Ви також можете ігнорувати git і просто завантажити код, також має спрацювати добре. Після цього Ви готові до встановлення.
Зверніть увагу, що Photos API також потребує MongoDB, тому має сенс спочатку встановити й налаштувати Mongo. > У цьому 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`
Щоб запустити бота, вам потрібно мати інтерпретатор Python, Photos API, MongoDB і, за бажанням, git (якщо ви хочете оновлювати за допомогою `git pull`). Ви також можете проігнорувати git і просто завантажити вихідний код, це також повинно спрацювати. Після цього ви готові до роботи. 2. Встановіть залежності:
`python -m pip install -r requirements.txt`
Без їх установки бот не зможе працювати взагалі
> У цьому README я припускаю, що ви використовуєте python за замовчуванням у вашій 3. Встановіть додаткові залежності [Не обов'язково]:
> системі, і він міститься у вашому системному PATH. Якщо ваш python за замовчуванням `python -m pip install -r requirements-optional.txt`
> це `python3` або, наприклад, `/home/user/.local/bin/python3.9` - використовуйте його. Вони не є обов’язковими, але можуть прискорити роботу бота
> Якщо це нестандартний шлях до виконуваного файлу - вам також слід змінити
> його у скриптах, які ви будете використовувати (`loop.sh`, `loop.bat`, `start.sh` та `start.bat`).
1. Встановіть MongoDB та Photos API: 4. Налаштуйте свого бота за допомогою текстового редактора:
`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`.
1. Встановіть MongoDB, дотримуючись [офіційного посібника зі встановлення](https://www.mongodb.com/docs/manual/installation) 5. Додайте бота на канал:
2. Встановіть Photos API, дотримуючись [README Photos API](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md) Звичайно, щоб використовувати свого бота, Вам потрібно мати канал або групу, інакше немає сенсу мати такого бота. [Тут](https://stackoverflow.com/a/33497769) Ви можете знайти короткий гайд, як додати свого бота до каналу.
2. Завантажте бота: 6. Заповніть папку вмістом:
Звичайно, бот не може опублікувати щось із нічого. Налаштуйте свій `config.json`, які медіа-типи бот повинен публікувати (`"posting", "extensions"`), коли їх публікувати (`"posting", "time"`), а також де їх знайти (`"locations"`). Ви також можете переміщати їх після надсилання, встановивши для `"posting", "move_sent"` значення `true`.
1. `git clone https://git.end-play.xyz/profitroll/TelegramPoster.git` (якщо ви використовуєте git) 6. Готово, запускайте!
2. `cd TelegramPoster` `python ./main.py`
Або ви також можете використовувати `.\start.bat` на Windows і `bash ./start.sh` на Linux.
Крім того, доступні `loop.sh` і `loop.bat`, якщо ви хочете, щоб ваш бот запускався знову після зупинки або після використання команди `/reboot`.
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. Встановіть залежності проекту: * `--move-sent` - дозволяє перемістити всі надіслані файли з черги до папки надісланих
* `--cleanup` - очистити файли в папках `queue` і `sent`, якщо вони вже надіслані. Потрібен аргумент `--confirm`
`python -m pip install -r requirements.txt`. * `--cleanup-index` - видалити всі надіслані записи з індексу. Потрібен аргумент `--confirm`
Без їх встановлення бот не зможе працювати взагалі. * `--norun` - дозволяє виконувати наведені вище аргументи, не запускаючи самого бота
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"`).
Приклади: Приклади:
* `python main.py --create-user` * `python3 ./main.py --move-sent --norun`
* `python main.py --create-user --create-album` * `python3 ./main.py --cleanup --confirm`
## Поради та покращення
* Можливо, ви захочете налаштувати бота для роботи як системну службу. У вікі є [сторінка з цього питання](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`, якщо в конфігураційному файлі не вказано іншу. Просто скопіюйте цікавлячий Вас файл, назвіть його відповідно до [тегів мови IETF](https://en.wikipedia.org/wiki/IETF_language_tag) (якщо Ви хочете, щоб переклад був сумісним з перекладами Telegram) або просто вкажіть свою власну назву. Збережіть свій переклад як json файл і все готово. Якщо ви хочете змінити мову за замовчуванням для повідомлень самого бота, які не можуть визначити мову адміністратора, відредагуйте параметр `"locale"` у `config.json`. Якщо ця мова недоступна, замість неї буде використано `"locale_fallback"`. Якщо обидві мови недоступні - буде показано помилку. Для зміни мови виведення консолі та логування вам слід відредагувати `"locale_log"`.
Ми рекомендуємо вносити зміни лише у вашу власну локаль. Або, принаймні, завжди мати резервну копію, наприклад, `en.json` як запасний варіант. Ми рекомендуємо вносити будь-які зміни лише до вашої окремої мови. Або, принаймні, завжди мати резервну копію, наприклад, `en.json` як запасний варіант.

View File

@@ -11,14 +11,12 @@ from typing import Dict, List, Tuple, Union
import aiofiles import aiofiles
from aiohttp import ClientSession from aiohttp import ClientSession
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from bson import ObjectId from bson import ObjectId
from libbot import json_write from libbot import json_write
from libbot.i18n.sync import _ from libbot.i18n.sync import _
from libbot.pyrogram.classes import PyroClient
from photosapi_client.errors import UnexpectedStatus from photosapi_client.errors import UnexpectedStatus
from pyrogram.errors import bad_request_400 from pyrogram.errors import bad_request_400
from pyrogram.types import Message, User from pyrogram.types import Message
from pytimeparse.timeparse import timeparse from pytimeparse.timeparse import timeparse
from ujson import dumps, loads from ujson import dumps, loads
@@ -28,7 +26,6 @@ from classes.exceptions import (
SubmissionUnavailableError, SubmissionUnavailableError,
SubmissionUnsupportedError, SubmissionUnsupportedError,
) )
from classes.pyrouser import PyroUser
from modules.api_client import ( from modules.api_client import (
BodyPhotoUpload, BodyPhotoUpload,
BodyVideoUpload, BodyVideoUpload,
@@ -39,25 +36,32 @@ from modules.api_client import (
photo_upload, photo_upload,
video_upload, video_upload,
) )
from modules.database import col_submitted, col_users from modules.database import col_submitted
from modules.http_client import http_session from modules.http_client import http_session
from modules.sender import send_content from modules.sender import send_content
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from datetime import datetime
from typing import List, Union
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from libbot.pyrogram.classes import PyroClient
class PyroClient(PyroClient): class PyroClient(PyroClient):
def __init__(self, scheduler: AsyncIOScheduler): def __init__(self, scheduler: AsyncIOScheduler):
super().__init__(locales_root=Path("locale"), scheduler=scheduler) super().__init__(scheduler=scheduler)
self.version: float = 0.3 self.version: float = 0.2
self.owner: int = self.config["bot"]["owner"] self.owner: int = self.config["bot"]["owner"]
self.admins: List[int] = self.config["bot"]["admins"] + [ self.admins: List[int] = self.config["bot"]["admins"] + [
self.config["bot"]["owner"] self.config["bot"]["owner"]
] ]
self.sender_session: Union[ClientSession, None] = None self.sender_session = ClientSession()
self.scopes_placeholders: Dict[str, int] = { self.scopes_placeholders: Dict[str, int] = {
"owner": self.owner, "owner": self.owner,
@@ -67,9 +71,6 @@ class PyroClient(PyroClient):
async def start(self): async def start(self):
await super().start() await super().start()
if self.sender_session is None:
self.sender_session = ClientSession()
if self.config["reports"]["update"]: if self.config["reports"]["update"]:
try: try:
async with ClientSession( async with ClientSession(
@@ -107,8 +108,8 @@ class PyroClient(PyroClient):
logger.warning( logger.warning(
"Could not send startup message to bot owner. Perhaps user has not started the bot yet." "Could not send startup message to bot owner. Perhaps user has not started the bot yet."
) )
except Exception as exc: except Exception as exp:
logger.exception("Update check failed due to %s: %s", exc, format_exc()) logger.exception("Update check failed due to %s: %s", exp, format_exc())
if self.config["mode"]["post"]: if self.config["mode"]["post"]:
if self.config["posting"]["use_interval"]: if self.config["posting"]["use_interval"]:
@@ -137,16 +138,14 @@ class PyroClient(PyroClient):
) )
await http_session.close() await http_session.close()
if self.sender_session is not None:
await self.sender_session.close() await self.sender_session.close()
await super().stop() await super().stop()
async def submit_media( async def submit_media(
self, id: str, purge_caption: bool = False self, id: str
) -> Tuple[Union[Message, None], Union[str, None]]: ) -> Tuple[Union[Message, None], Union[str, None]]:
db_entry = await col_submitted.find_one({"_id": ObjectId(id)}) db_entry = col_submitted.find_one({"_id": ObjectId(id)})
submission = None submission = None
if db_entry is None: if db_entry is None:
@@ -160,8 +159,8 @@ class PyroClient(PyroClient):
filepath = await self.download_media( filepath = await self.download_media(
submission, file_name=self.config["locations"]["tmp"] + sep submission, file_name=self.config["locations"]["tmp"] + sep
) )
except Exception as exc: except Exception as exp:
raise SubmissionUnavailableError() from exc raise SubmissionUnavailableError() from exp
elif not Path( elif not Path(
f"{self.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}/{db_entry['temp']['file']}", f"{self.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}/{db_entry['temp']['file']}",
@@ -184,7 +183,7 @@ class PyroClient(PyroClient):
response = await photo_upload( response = await photo_upload(
self.config["posting"]["api"]["album"], self.config["posting"]["api"]["album"],
client=client, client=client,
body=BodyPhotoUpload( multipart_data=BodyPhotoUpload(
File(media_bytes, filepath.name, "image/jpeg") File(media_bytes, filepath.name, "image/jpeg")
), ),
ignore_duplicates=self.config["submission"]["allow_duplicates"], ignore_duplicates=self.config["submission"]["allow_duplicates"],
@@ -195,20 +194,22 @@ class PyroClient(PyroClient):
response = await video_upload( response = await video_upload(
self.config["posting"]["api"]["album"], self.config["posting"]["api"]["album"],
client=client, client=client,
body=BodyVideoUpload(File(media_bytes, filepath.name, "video/*")), multipart_data=BodyVideoUpload(
File(media_bytes, filepath.name, "video/*")
),
caption="queue", caption="queue",
) )
# elif db_entry["type"] == SubmissionType.ANIMATION.value: # elif db_entry["type"] == SubmissionType.ANIMATION.value:
# response = await video_upload( # response = await video_upload(
# self.config["posting"]["api"]["album"], # self.config["posting"]["api"]["album"],
# client=client, # client=client,
# body=BodyVideoUpload( # multipart_data=BodyVideoUpload(
# File(media_bytes, filepath.name, "video/*") # File(media_bytes, filepath.name, "video/*")
# ), # ),
# caption="queue", # caption="queue",
# ) # )
except UnexpectedStatus as exc: except UnexpectedStatus as exp:
raise SubmissionUnsupportedError(str(filepath)) from exc raise SubmissionUnsupportedError(str(filepath)) from exp
response_dict = ( response_dict = (
{} {}
@@ -229,14 +230,10 @@ class PyroClient(PyroClient):
) )
raise SubmissionDuplicatesError(str(filepath), duplicates) raise SubmissionDuplicatesError(str(filepath), duplicates)
db_update = ( col_submitted.find_one_and_update(
{"$set": {"done": True, "caption": None}} {"_id": ObjectId(id)}, {"$set": {"done": True}}
if purge_caption
else {"$set": {"done": True}}
) )
await col_submitted.update_one({"_id": ObjectId(id)}, db_update)
try: try:
if db_entry["temp"]["uuid"] is not None: if db_entry["temp"]["uuid"] is not None:
rmtree( rmtree(
@@ -252,36 +249,11 @@ class PyroClient(PyroClient):
return ( return (
submission, submission,
response.parsed.id if hasattr(response, "parsed") else response.id, response.id if not hasattr(response, "parsed") else response.parsed.id,
) )
async def find_user(self, user: Union[int, User]) -> PyroUser: async def ban_user(self, id: int) -> None:
"""Find User by it's ID or User object pass
### Args: async def unban_user(self, id: int) -> None:
* user (`Union[int, User]`): ID or User object to extract ID from pass
### Returns:
* `PyroUser`: PyroUser object
"""
if (
await col_users.find_one(
{"id": user.id if isinstance(user, User) else user}
) # type: ignore
is None
):
await 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 = await col_users.find_one(
{"id": user.id if isinstance(user, User) else user}
) # type: ignore
return PyroUser(**db_record)

View File

@@ -1,53 +0,0 @@
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) -> None:
await col_users.update_one({"_id": self._id}, {"$set": {"locale": locale}})
async def update_cooldown(self, time: datetime = datetime.now()) -> None:
await col_users.update_one({"_id": self._id}, {"$set": {"cooldown": time}})
async def block(self) -> None:
"""Ban user from using command and submitting content."""
await col_users.update_one({"_id": self._id}, {"$set": {"banned": True}})
async def unblock(self) -> None:
"""Allow user to use command and submit posts again."""
await 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 = (
await config_get("admins", "bot") + [await config_get("owner", "bot")]
if app is None
else app.admins
)
return (datetime.now() - self.cooldown).total_seconds() < (
app.config["submission"]["timeout"]
if app is not None
else await config_get("timeout", "submission")
)

58
classes/user.py Normal file
View File

@@ -0,0 +1,58 @@
from datetime import datetime
from libbot import sync
from modules.database import col_banned, col_users
class PosterUser:
def __init__(self, id: int):
self.id = id
def is_blocked(self) -> bool:
"""Check if user is banned from submitting content.
### Returns:
`bool`: Must be `True` if banned and `False` if not
"""
return False if col_banned.find_one({"user": self.id}) is None else True
def block(self) -> None:
"""Ban user from using command and submitting content."""
if col_banned.find_one({"user": self.id}) is None:
col_banned.insert_one({"user": self.id, "date": datetime.now()})
def unblock(self) -> None:
"""Allow user to use command and submit posts again."""
col_banned.find_one_and_delete({"user": self.id})
def is_limited(self) -> bool:
"""Check if user is on a cooldown after submitting something.
### Returns:
`bool`: Must be `True` if on the cooldown and `False` if not
"""
if self.id in sync.config_get("admins", "bot"):
return False
db_record = col_users.find_one({"user": self.id})
if db_record is None:
return False
return (
True
if (datetime.now() - db_record["cooldown"]).total_seconds()
< sync.config_get("timeout", "submission")
else False
)
def limit(self) -> None:
"""Restart user's cooldown. Used after post has been submitted."""
if (
col_users.find_one_and_update(
{"user": self.id}, {"$set": {"cooldown": datetime.now()}}
)
is None
):
col_users.insert_one({"user": self.id, "cooldown": datetime.now()})

View File

@@ -1,12 +1,14 @@
{ {
"locale": "en", "locale": "en",
"locale_log": "en",
"locale_fallback": "en",
"bot": { "bot": {
"owner": 0, "owner": 0,
"admins": [], "admins": [],
"api_id": 0, "api_id": 0,
"api_hash": "", "api_hash": "",
"bot_token": "", "bot_token": "",
"max_concurrent_transmissions": 1, "max_concurrent_transmissions": 5,
"scoped_commands": true "scoped_commands": true
}, },
"database": { "database": {
@@ -17,7 +19,7 @@
"name": "tgposter" "name": "tgposter"
}, },
"reports": { "reports": {
"chat_id": "owner", "chat_id": 0,
"sent": false, "sent": false,
"error": true, "error": true,
"update": true, "update": true,
@@ -35,7 +37,11 @@
"locations": { "locations": {
"tmp": "tmp", "tmp": "tmp",
"data": "data", "data": "data",
"cache": "cache" "cache": "cache",
"sent": "data/sent",
"queue": "data/queue",
"index": "data/index.json",
"locale": "locale"
}, },
"disabled_plugins": [], "disabled_plugins": [],
"posting": { "posting": {
@@ -83,8 +89,7 @@
"address_external": "https://photos.domain.com", "address_external": "https://photos.domain.com",
"username": "", "username": "",
"password": "", "password": "",
"album": "", "album": ""
"timeout": 15.0
} }
}, },
"caption": { "caption": {
@@ -134,17 +139,6 @@
} }
] ]
}, },
"language": {
"scopes": [
{
"name": "BotCommandScopeDefault"
},
{
"name": "BotCommandScopeChat",
"chat_id": "owner"
}
]
},
"report": { "report": {
"scopes": [ "scopes": [
{ {

View File

@@ -1,17 +1,7 @@
{ {
"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",
"report": "Report this post", "report": "Report this post",
"forwards": "Check post forwards", "forwards": "Check post forwards",
"import": "Submit .zip archive with photos", "import": "Submit .zip archive with photos",
@@ -32,7 +22,6 @@
"sub_yes_auto": "✅ Submission automatically 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_dup": "⚠️ Submission automatically declined because database already contains this photo",
"sub_deleted": "⚠️ Submission's database record ({0}) is not available.",
"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:",
@@ -65,14 +54,11 @@
"import_upload_error_duplicate": "Could not upload `{0}` because there're duplicates on server.", "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_upload_error_other": "Could not upload `{0}`. Probably disallowed filetype.",
"import_finished": "Import finished.", "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_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_ignored": "No response, aborting removal.",
"remove_abort": "Removal aborted.", "remove_abort": "Removal aborted.",
"remove_success": "Removed media with ID `{0}`.", "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_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.", "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.", "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_sent": "We've notified admins about presumable violation. Thank you for cooperation.",
@@ -87,9 +73,7 @@
"post_view": "View in channel", "post_view": "View in channel",
"accepted": "✅ Accepted", "accepted": "✅ Accepted",
"declined": "❌ Declined", "declined": "❌ Declined",
"shutdown": "Confirm shutdown", "shutdown": "Confirm shutdown"
"photo": "Photo",
"video": "Video"
}, },
"callback": { "callback": {
"sub_yes": "✅ Submission approved", "sub_yes": "✅ Submission approved",
@@ -100,7 +84,50 @@
"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", "sub_done": "You've already decided what to do with submission",
"sub_duplicates_found": "There're duplicates in bot's database", "sub_duplicates_found": "There're duplicates in bot's database",
"locale_set": "Your language now is: {locale}",
"nothing": "🏁 This action is already finished" "nothing": "🏁 This action is already finished"
},
"console": {
"shutdown": "Shutting down bot with pid {0}",
"startup": "Starting with pid {0}",
"keyboard_interrupt": "\nShutting down...",
"exception_occured": "Exception {0} happened on task execution",
"post_sent": "Sent {0} to {1} with caption {2} and silently {3}",
"post_exception": "Could not send content due to {0}. Traceback: {1}",
"post_invalid_pic": "Error while sending photo HTTP {0}: {1}",
"post_empty": "Could not send content due to queue empty or contains only forbidden extensions",
"sub_mime_not_allowed": "Got submission from {0} but type of {1} which is not allowed",
"sub_document_too_large": "Got submission from {0} but but file is too large ({1} > {2})",
"sub_received": "Got submission from {0} with a caption {1}",
"sub_cooldown": "Got submission from {0} but user is on a cooldown",
"sub_no_id": "from_user in function get_submission does not contain id (maybe user posted in a channel)",
"sub_msg_unavail": "Could not download submission {0} from user {1}: message not available",
"sub_media_unavail": "Could not download submission {0} from user {1}: media not available",
"sub_media_downloading": "Downloading media of submission {0} from user {1}...",
"sub_media_downloaded": "Downloaded media of submission {0} from user {1}",
"sub_accepted": "Accepted submission {0} from user {1}",
"sub_declined": "Declined submission {0} from user {1}",
"sub_blocked": "Blocked user {0}",
"sub_unblocked": "Unblocked user {0}",
"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",
"random_pic_response": "Random pic response: {0}",
"random_pic_error_code": "Could not get photos from album {0}: HTTP {1}",
"random_pic_error_debug": "Could not get photos from '{0}/albums/{1}/photos?q=&page_size={2}&caption=queue' using token '{3}': HTTP {4}",
"find_pic_error": "Could not find image with name '{0}' and caption '{1}' due to: {2}",
"pic_upload_error": "Could not upload '{0}' to API: HTTP {1} with message '{2}'",
"api_creds_invalid": "Incorrect API credentials! Could not login into '{0}' using login '{1}': HTTP {2}",
"user_blocked": "User {0} has been blocked",
"user_unblocked": "User {0} has been unblocked",
"submission_accepted": "Submission with ID '{0}' accepted and uploaded with ID '{1}'",
"submission_rejected": "Submission with ID '{0}' rejected",
"submission_duplicate": "Submission with ID '{0}' could not be accepted because of the duplicates: {1}"
} }
} }

View File

@@ -1,16 +1,7 @@
{ {
"metadata": {
"flag": "🇺🇦",
"name": "Українська",
"codes": [
"uk",
"uk-UA"
]
},
"commands": { "commands": {
"start": "Почати користуватись ботом", "start": "Почати користуватись ботом",
"rules": "Правила пропонування фото", "rules": "Правила пропонування фото",
"language": "Змінити мову бота",
"report": "Поскаржитись на цей пост", "report": "Поскаржитись на цей пост",
"forwards": "Переглянути репости", "forwards": "Переглянути репости",
"import": "Надати боту .zip архів з фотографіями", "import": "Надати боту .zip архів з фотографіями",
@@ -31,7 +22,6 @@
"sub_yes_auto": "✅ Подання автоматично прийнято", "sub_yes_auto": "✅ Подання автоматично прийнято",
"sub_no": "❌ Подання розглянуто та відхилено", "sub_no": "❌ Подання розглянуто та відхилено",
"sub_dup": "⚠️ Подання автоматично відхилено через наявність цього фото в базі даних", "sub_dup": "⚠️ Подання автоматично відхилено через наявність цього фото в базі даних",
"sub_deleted": "⚠️ Запис подання у базі даних ({0}) недоступний.",
"sub_blocked": "Вас заблокували, ви більше не можете надсилати медіафайли.", "sub_blocked": "Вас заблокували, ви більше не можете надсилати медіафайли.",
"sub_unblocked": "Вас розблокували, тепер ви можете надсилати медіафайли.", "sub_unblocked": "Вас розблокували, тепер ви можете надсилати медіафайли.",
"sub_by": "\n\nПредставлено:", "sub_by": "\n\nПредставлено:",
@@ -64,14 +54,11 @@
"import_upload_error_duplicate": "Не вдалося завантажити `{0}`, оскільки на сервері є дублікати.", "import_upload_error_duplicate": "Не вдалося завантажити `{0}`, оскільки на сервері є дублікати.",
"import_upload_error_other": "Не вдалося завантажити `{0}`. Ймовірно, заборонений тип файлу.", "import_upload_error_other": "Не вдалося завантажити `{0}`. Ймовірно, заборонений тип файлу.",
"import_finished": "Імпорт завершено.", "import_finished": "Імпорт завершено.",
"locale_choice": "Гаразд. Будь ласка, оберіть мову за допомогою клавіатури нижче.",
"remove_request": "Будь ласка, надішліть мені ID для видалення. Ви могли отримати його з діалогу завантаження. Використовуйте /cancel, якщо ви хочете перервати цю операцію.", "remove_request": "Будь ласка, надішліть мені ID для видалення. Ви могли отримати його з діалогу завантаження. Використовуйте /cancel, якщо ви хочете перервати цю операцію.",
"remove_ignored": "Немає відповіді, перериваємо видалення.", "remove_ignored": "Немає відповіді, перериваємо видалення.",
"remove_abort": "Видалення перервано.", "remove_abort": "Видалення перервано.",
"remove_success": "Видалено медіа з ID `{0}`.", "remove_success": "Видалено медіа з ID `{0}`.",
"remove_failure": "Не вдалося видалити медіа з ID `{0}`. Перевірте, чи вказано правильний ID, і якщо він правильний, ви також можете переглянути логи бота для отримання більш детальної інформації.", "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` у конфігурації.", "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} незавершених контекстів користувачів. Якщо ви вимкнете бота, вони будуть втрачені. Будь ласка, підтвердіть вимкнення за допомогою кнопки нижче.", "shutdown_confirm": "Існує {0} незавершених контекстів користувачів. Якщо ви вимкнете бота, вони будуть втрачені. Будь ласка, підтвердіть вимкнення за допомогою кнопки нижче.",
"report_sent": "Ми повідомили адміністрацію про потенційне порушення. Дякую за співпрацю.", "report_sent": "Ми повідомили адміністрацію про потенційне порушення. Дякую за співпрацю.",
@@ -86,9 +73,7 @@
"post_view": "Переглянути на каналі", "post_view": "Переглянути на каналі",
"accepted": "✅ Прийнято", "accepted": "✅ Прийнято",
"declined": "❌ Відхилено", "declined": "❌ Відхилено",
"shutdown": "Підтвердити вимкнення", "shutdown": "Підтвердити вимкнення"
"photo": "Фото",
"video": "Відео"
}, },
"callback": { "callback": {
"sub_yes": "✅ Подання схвалено", "sub_yes": "✅ Подання схвалено",
@@ -99,7 +84,50 @@
"sub_media_unavail": "Не вдалося завантажити подання", "sub_media_unavail": "Не вдалося завантажити подання",
"sub_done": "Ви вже обрали що зробити з цим поданням", "sub_done": "Ви вже обрали що зробити з цим поданням",
"sub_duplicates_found": "Знайдено дублікати в базі даних бота", "sub_duplicates_found": "Знайдено дублікати в базі даних бота",
"locale_set": "Встановлено мову: {locale}",
"nothing": "🏁 Цю дію вже було завершено" "nothing": "🏁 Цю дію вже було завершено"
},
"console": {
"shutdown": "Вимкнення бота з підом {0}",
"startup": "Запуск бота з підом {0}",
"keyboard_interrupt": "\nВимикаюсь...",
"exception_occured": "Помилка {0} сталась під час виконання",
"post_sent": "Надіслано {0} у {1} з підписом {2} та без звуку {3}",
"post_exception": "Не вдалося надіслати контент через {0}. Traceback: {1}",
"post_invalid_pic": "Помилка надсилання фото HTTP {0}: {1}",
"post_empty": "Не вдалося надіслати контент адже черга з дозволеними розширеннями порожня",
"sub_mime_not_allowed": "Отримано подання від {0} але типу {1} який не є дозволеним",
"sub_document_too_large": "Отримано подання від {0} але файл завеликий({1} > {2})",
"sub_received": "Отримано подання від {0} з підписом {1}",
"sub_cooldown": "Отримано подання від {0} але користувач на тайм-ауті",
"sub_no_id": "from_user у функції get_submission не має атрибуту id (можливо, користувач запостив щось у канал)",
"sub_msg_unavail": "Не вдалося завантажити подання {0} від користувача {1}: повідомлення більше не існує",
"sub_media_unavail": "Не вдалося завантажити подання {0} від користувача {1}: медіафайл більше не існує",
"sub_media_downloading": "Завантажуємо медіа з подання {0} від користувача{1}...",
"sub_media_downloaded": "Завантажено медіа з подання{0} від користувача{1}",
"sub_accepted": "Прийнято подання {0} від користувача {1}",
"sub_declined": "Відхилено подання {0} від користувача {1}",
"sub_blocked": "Заблоковано користувача {0}",
"sub_unblocked": "Розблоковано користувача {0}",
"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'",
"random_pic_response": "Відповідь на пошук випадкової картинки: {0}",
"random_pic_error_code": "Не вдалося отримати фото з альбому {0}: HTTP {1}",
"random_pic_error_debug": "Не вдалося отримати фотографії з '{0}/albums/{1}/photos?q=&page_size={2}&caption=queue', використовуючи токен '{3}': HTTP {4}",
"find_pic_error": "Не вдалося знайти зображення з назвою '{0}' та підписом '{1}' через: {2}",
"pic_upload_error": "Не вдалося завантажити '{0}' до API: HTTP {1} з повідомленням '{2}'",
"api_creds_invalid": "Невірні облікові дані API! Не вдалося увійти в '{0}' за допомогою логіна '{1}': HTTP {2}",
"user_blocked": "Користувача {0} було заблоковано",
"user_unblocked": "Користувача {0} було розблоковано",
"submission_accepted": "Подання з ID '{0}' прийнято та завантажено з ID '{1}'",
"submission_rejected": "Подання з ID '{0}' відхилено",
"submission_duplicate": "Подання з ID '{0}' не може бути прийнято через наявність дублікатів: {1}"
} }
} }

View File

@@ -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 main.py python poster.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

View File

@@ -5,7 +5,7 @@
while true while true
do do
python main.py python poster.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

View File

@@ -4,10 +4,6 @@ from os import getpid
from convopyro import Conversation 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 classes.pyroclient import PyroClient
from modules.scheduler import scheduler from modules.scheduler import scheduler

View File

@@ -35,9 +35,6 @@ from photosapi_client.api.default.photo_upload_albums_album_photos_post import (
) )
from photosapi_client.api.default.user_create_users_post import asyncio as user_create 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.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 ( from photosapi_client.api.default.video_find_albums_album_videos_get import (
asyncio as video_find, asyncio as video_find,
) )
@@ -100,10 +97,15 @@ async def authorize(custom_session: Union[ClientSession, None] = None) -> str:
) )
if not response.ok: if not response.ok:
logger.warning( logger.warning(
"Incorrect API credentials! Could not login into '%s' using login '%s': HTTP %s", i18n._(
"api_creds_invalid",
"console",
locale=(await config_get("locale_log")).format(
await config_get("address", "posting", "api"), await config_get("address", "posting", "api"),
await config_get("username", "posting", "api"), await config_get("username", "posting", "api"),
response.status, response.status,
),
)
) )
raise ValueError raise ValueError
async with aiofiles.open( async with aiofiles.open(
@@ -117,14 +119,15 @@ async def authorize(custom_session: Union[ClientSession, None] = None) -> str:
unauthorized_client = Client( unauthorized_client = Client(
sync.config_get("address", "posting", "api"), base_url=sync.config_get("address", "posting", "api"),
timeout=5.0,
verify_ssl=True,
raise_on_unexpected_status=True, raise_on_unexpected_status=True,
timeout=sync.config_get("timeout", "posting", "api"),
) )
login_token = login( login_token = login(
client=unauthorized_client, client=unauthorized_client,
body=BodyLoginForAccessTokenTokenPost( form_data=BodyLoginForAccessTokenTokenPost(
grant_type="password", grant_type="password",
scope="me albums.list albums.read albums.write photos.list photos.read photos.write videos.list videos.read videos.write", 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"), username=sync.config_get("username", "posting", "api"),
@@ -139,10 +142,11 @@ if not isinstance(login_token, Token):
exit() exit()
client = AuthenticatedClient( client = AuthenticatedClient(
sync.config_get("address", "posting", "api"), base_url=sync.config_get("address", "posting", "api"),
token=login_token.access_token, timeout=5.0,
verify_ssl=True,
raise_on_unexpected_status=True, raise_on_unexpected_status=True,
timeout=sync.config_get("timeout", "posting", "api"), token=login_token.access_token,
) )
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,118 +0,0 @@
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()
if args.create_user or args.create_album:
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 exc:
print(f"Could not create a user due to {exc}", 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 exc:
print(f"Could not create an album due to {exc}", 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()

View File

@@ -1,18 +0,0 @@
"""Custom message filters"""
from pyrogram import filters
from pyrogram.types import Message
from classes.pyroclient import PyroClient
async def _mode_post_func(_, __: PyroClient, message: Message):
return __.config["mode"]["post"]
async def _mode_submit_func(_, __: PyroClient, message: Message):
return __.config["mode"]["submit"]
mode_post = filters.create(_mode_post_func)
mode_submit = filters.create(_mode_submit_func)

View File

@@ -1,9 +1,11 @@
"""Module that provides all database columns""" """Module that provides all database columns"""
from async_pymongo import AsyncClient from pymongo import MongoClient
from libbot import sync from ujson import loads
db_config = sync.config_get("database") 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: if db_config["user"] is not None and db_config["password"] is not None:
con_string = "mongodb://{0}:{1}@{2}:{3}/{4}".format( con_string = "mongodb://{0}:{1}@{2}:{3}/{4}".format(
@@ -18,9 +20,16 @@ else:
db_config["host"], db_config["port"], db_config["name"] db_config["host"], db_config["port"], db_config["name"]
) )
db_client = AsyncClient(con_string) db_client = MongoClient(con_string)
db = db_client.get_database(name=db_config["name"]) db = db_client.get_database(name=db_config["name"])
collections = db.list_collection_names()
for collection in ["sent", "users", "banned", "submitted"]:
if not collection in collections:
db.create_collection(collection)
col_sent = db.get_collection("sent") col_sent = db.get_collection("sent")
col_users = db.get_collection("users") col_users = db.get_collection("users")
col_banned = db.get_collection("banned")
col_submitted = db.get_collection("submitted") col_submitted = db.get_collection("submitted")

View File

@@ -77,12 +77,12 @@ async def send_content(app: PyroClient, http_session: ClientSession) -> None:
try: try:
response: File = await func_iter[1](id=media.id, client=client) response: File = await func_iter[1](id=media.id, client=client)
except Exception as exc: except Exception as exp:
print_exc() print_exc()
logger.error("Media is invalid: %s", exc) logger.error("Media is invalid: %s", exp)
if app.config["reports"]["error"]: if app.config["reports"]["error"]:
await app.send_message( await app.send_message(
app.owner, f"Media is invalid: {exc}" app.owner, f"Media is invalid: {exp}"
) )
return return
@@ -103,17 +103,15 @@ async def send_content(app: PyroClient, http_session: ClientSession) -> None:
).results[0] ).results[0]
try: try:
response: File = await func[1](id=media.id, client=client) response: File = await func[1](id=media.id, client=client)
except Exception as exc: except Exception as exp:
print_exc() print_exc()
logger.error("Media is invalid: %s", exc) logger.error("Media is invalid: %s", exp)
if app.config["reports"]["error"]: if app.config["reports"]["error"]:
await app.send_message(app.owner, f"Media is invalid: {exc}") await app.send_message(app.owner, f"Media is invalid: {exp}")
return return
except (KeyError, AttributeError, TypeError, IndexError): except (KeyError, AttributeError, TypeError, IndexError):
logger.info( logger.info(app._("post_empty", "console"))
"Could not send content due to queue empty or contains only forbidden extensions"
)
if app.config["reports"]["error"]: if app.config["reports"]["error"]:
await app.send_message( await app.send_message(
app.owner, app.owner,
@@ -152,7 +150,7 @@ async def send_content(app: PyroClient, http_session: ClientSession) -> None:
) and func[0] is photo_random: ) and func[0] is photo_random:
image = Image.open(path.join(app.config["locations"]["tmp"], tmp_path)) image = Image.open(path.join(app.config["locations"]["tmp"], tmp_path))
width, height = image.size width, height = image.size
image = image.resize((int(width / 2), int(height / 2)), Image.LANCZOS) image = image.resize((int(width / 2), int(height / 2)), Image.ANTIALIAS)
if tmp_path.lower().endswith(".jpeg") or tmp_path.lower().endswith(".jpg"): if tmp_path.lower().endswith(".jpeg") or tmp_path.lower().endswith(".jpg"):
image.save( image.save(
path.join(app.config["locations"]["tmp"], tmp_path), path.join(app.config["locations"]["tmp"], tmp_path),
@@ -179,7 +177,7 @@ async def send_content(app: PyroClient, http_session: ClientSession) -> None:
del response del response
submitted = await col_submitted.find_one({"temp.file": media.filename}) submitted = col_submitted.find_one({"temp.file": media.filename})
if submitted is not None and submitted["caption"] is not None: if submitted is not None and submitted["caption"] is not None:
caption = submitted["caption"].strip() caption = submitted["caption"].strip()
@@ -217,19 +215,19 @@ async def send_content(app: PyroClient, http_session: ClientSession) -> None:
caption=caption, caption=caption,
disable_notification=app.config["posting"]["silent"], disable_notification=app.config["posting"]["silent"],
) )
except Exception as exc: except Exception as exp:
logger.error( logger.error(
"Could not send media %s (%s) due to %s", media.filename, media.id, exc "Could not send media %s (%s) due to %s", media.filename, media.id, exp
) )
if app.config["reports"]["error"]: if app.config["reports"]["error"]:
await app.send_message( await app.send_message(
app.owner, app.owner,
app._("post_exception", "message").format(exc, format_exc()), app._("post_exception", "message").format(exp, format_exc()),
) )
# rmtree(path.join(app.config['locations']['tmp'], tmp_dir), ignore_errors=True) # rmtree(path.join(app.config['locations']['tmp'], tmp_dir), ignore_errors=True)
return return
await col_sent.insert_one( col_sent.insert_one(
{ {
"date": datetime.now(), "date": datetime.now(),
"image": media.id, "image": media.id,
@@ -246,21 +244,20 @@ async def send_content(app: PyroClient, http_session: ClientSession) -> None:
rmtree(path.join(app.config["locations"]["tmp"], tmp_dir), ignore_errors=True) rmtree(path.join(app.config["locations"]["tmp"], tmp_dir), ignore_errors=True)
logger.info( logger.info(
"Sent %s to %s with caption %s and silently %s", app._("post_sent", "console").format(
media.id, media.id,
str(app.config["posting"]["channel"]), str(app.config["posting"]["channel"]),
caption.replace("\n", "%n"), caption.replace("\n", "%n"),
str(app.config["posting"]["silent"]), str(app.config["posting"]["silent"]),
) )
except Exception as exc:
logger.error(
"Could not send content due to %s. Traceback: %s", exc, format_exc()
) )
except Exception as exp:
logger.error(app._("post_exception", "console").format(str(exp), format_exc()))
if app.config["reports"]["error"]: if app.config["reports"]["error"]:
await app.send_message( await app.send_message(
app.owner, app.owner,
app._("post_exception", "message").format(exc, format_exc()), app._("post_exception", "message").format(exp, format_exc()),
) )
try: try:
rmtree( rmtree(

View File

@@ -6,7 +6,7 @@ from classes.pyroclient import PyroClient
@Client.on_callback_query(filters.regex("nothing")) @Client.on_callback_query(filters.regex("nothing"))
async def callback_query_nothing(app: PyroClient, callback: CallbackQuery): async def callback_query_nothing(app: PyroClient, clb: CallbackQuery):
user = await app.find_user(callback.from_user) await clb.answer(
text=app._("nothing", "callback", locale=clb.from_user.language_code)
await callback.answer(text=app._("nothing", "callback", locale=user.locale)) )

View File

@@ -10,11 +10,11 @@ from classes.pyroclient import PyroClient
@Client.on_callback_query(filters.regex("shutdown")) @Client.on_callback_query(filters.regex("shutdown"))
async def callback_query_nothing(app: PyroClient, callback: CallbackQuery): async def callback_query_nothing(app: PyroClient, clb: CallbackQuery):
if callback.from_user.id not in app.admins: if clb.from_user.id not in app.admins:
return return
await callback.answer() await clb.answer()
makedirs(await config_get("cache", "locations"), exist_ok=True) makedirs(await config_get("cache", "locations"), exist_ok=True)
await json_write( await json_write(

View File

@@ -15,76 +15,68 @@ from classes.exceptions import (
SubmissionUnsupportedError, SubmissionUnsupportedError,
) )
from classes.pyroclient import PyroClient from classes.pyroclient import PyroClient
from classes.user import PosterUser
from modules.database import col_submitted from modules.database import col_submitted
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@Client.on_callback_query(filters.regex("sub_yes_[\s\S]*")) @Client.on_callback_query(filters.regex("sub_yes_[\s\S]*"))
async def callback_query_yes(app: PyroClient, callback: CallbackQuery): async def callback_query_yes(app: PyroClient, clb: CallbackQuery):
user = await app.find_user(callback.from_user) fullclb = str(clb.data).split("_")
fullcallback = str(callback.data).split("_") user_locale = clb.from_user.language_code
db_entry = await col_submitted.find_one({"_id": ObjectId(fullcallback[2])}) db_entry = col_submitted.find_one({"_id": ObjectId(fullclb[2])})
try: try:
submission = await app.submit_media( submission = await app.submit_photo(fullclb[2])
fullcallback[2],
purge_caption=("caption" not in fullcallback),
)
except SubmissionUnavailableError: except SubmissionUnavailableError:
await callback.answer( await clb.answer(
text=app._("sub_msg_unavail", "callback", locale=user.locale), text=app._("sub_msg_unavail", "callback", locale=user_locale),
show_alert=True, show_alert=True,
) )
return return
except SubmissionUnsupportedError: except SubmissionUnsupportedError:
await callback.answer( await clb.answer(
text=app._("mime_not_allowed", "message", locale=user.locale).format( text=app._("mime_not_allowed", "message", locale=user_locale).format(
", ".join(app.config["submission"]["mime_types"]), quote=True ", ".join(app.config["submission"]["mime_types"]), quote=True
), ),
show_alert=True, show_alert=True,
) )
return return
except SubmissionDuplicatesError as exc: except SubmissionDuplicatesError as exp:
await callback.answer( await clb.answer(
text=app._("sub_duplicates_found", "callback", locale=user.locale), text=app._("sub_duplicates_found", "callback", locale=user_locale),
show_alert=True, show_alert=True,
) )
await callback.message.reply_text( await clb.message.reply_text(
app._("sub_media_duplicates_list", "message", locale=user.locale).format( app._("sub_media_duplicates_list", "message", locale=user_locale).format(
"\n".join(exc.duplicates) "\n".join(exp.duplicates)
), ),
quote=True, quote=True,
) )
logger.info( logger.info(
"Submission with ID '%s' could not be accepted because of the duplicates: %s", app._(
fullcallback[2], "submission_duplicate",
str(exc.duplicates), "console",
locale=app.config["locale_log"],
).format(
fullclb[2],
str(exp.duplicates),
),
) )
return return
if submission[0] is not None: if submission[0] is not None:
await submission[0].reply_text( await submission[0].reply_text(
app._( app._("sub_yes", "message", locale=submission[0].from_user.language_code),
"sub_yes",
"message",
locale=(await app.find_user(submission[0].from_user)).locale,
),
quote=True, quote=True,
) )
elif db_entry is not None: elif db_entry is not None:
await app.send_message( await app.send_message(db_entry["user"], app._("sub_yes", "message"))
db_entry["user"],
app._(
"sub_yes",
"message",
locale=(await app.find_user(db_entry["user"])).locale,
),
)
await callback.answer( await clb.answer(
text=app._("sub_yes", "callback", locale=user.locale).format(fullcallback[2]), text=app._("sub_yes", "callback", locale=user_locale).format(fullclb[2]),
show_alert=True, show_alert=True,
) )
@@ -92,17 +84,17 @@ async def callback_query_yes(app: PyroClient, callback: CallbackQuery):
[ [
[ [
InlineKeyboardButton( InlineKeyboardButton(
text=str(app._("accepted", "button", locale=user.locale)), text=str(app._("accepted", "button", locale=user_locale)),
callback_data="nothing", callback_data="nothing",
) )
], ],
callback.message.reply_markup.inline_keyboard[1], clb.message.reply_markup.inline_keyboard[1],
] ]
if len(callback.message.reply_markup.inline_keyboard) > 1 if len(clb.message.reply_markup.inline_keyboard) > 1
else [ else [
[ [
InlineKeyboardButton( InlineKeyboardButton(
text=str(app._("accepted", "button", locale=user.locale)), text=str(app._("accepted", "button", locale=user_locale)),
callback_data="nothing", callback_data="nothing",
) )
] ]
@@ -110,44 +102,32 @@ async def callback_query_yes(app: PyroClient, callback: CallbackQuery):
) )
if await config_get("send_uploaded_id", "submission"): if await config_get("send_uploaded_id", "submission"):
await callback.message.edit_caption( await clb.message.edit_caption(
f"{callback.message.caption}\n\nID: `{submission[1]}`" clb.message.caption + f"\n\nID: `{submission[1]}`"
) )
await callback.message.edit_reply_markup( await clb.message.edit_reply_markup(
reply_markup=InlineKeyboardMarkup(edited_markup) reply_markup=InlineKeyboardMarkup(edited_markup)
) )
logger.info( logger.info(
"Submission with ID '%s' accepted and uploaded with ID '%s'", app._(
fullcallback[2], "submission_accepted",
submission[1], "console",
) locale=app.config["locale_log"],
logger.info( ).format(fullclb[2], submission[1]),
"Submission with ID '%s' accepted and uploaded with ID '%s'",
fullcallback[2],
submission[1],
) )
@Client.on_callback_query(filters.regex("sub_no_[\s\S]*")) @Client.on_callback_query(filters.regex("sub_no_[\s\S]*"))
async def callback_query_no(app: PyroClient, callback: CallbackQuery): async def callback_query_no(app: PyroClient, clb: CallbackQuery):
user = await app.find_user(callback.from_user) fullclb = str(clb.data).split("_")
fullcallback = str(callback.data).split("_") user_locale = clb.from_user.language_code
db_entry = await col_submitted.delete_one({"_id": ObjectId(fullcallback[2])}) db_entry = col_submitted.find_one_and_delete({"_id": ObjectId(fullclb[2])})
if db_entry.deleted_count == 0:
await callback.answer(
text=app._("sub_deleted", "callback", locale=user.locale).format(
fullcallback[2]
),
show_alert=True,
)
return
if ( if (
db_entry.raw_result["temp"]["uuid"] is not None db_entry["temp"]["uuid"] is not None
and Path( and Path(
f"{app.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}" f"{app.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}"
).exists() ).exists()
@@ -163,23 +143,19 @@ async def callback_query_no(app: PyroClient, callback: CallbackQuery):
submission = await app.get_messages( submission = await app.get_messages(
db_entry["user"], db_entry["telegram"]["msg_id"] db_entry["user"], db_entry["telegram"]["msg_id"]
) )
except Exception as exc: except Exception as exp:
await callback.answer( await clb.answer(
text=app._("sub_msg_unavail", "message", locale=user.locale), text=app._("sub_msg_unavail", "message", locale=user_locale),
show_alert=True, show_alert=True,
) )
return return
await submission.reply_text( await submission.reply_text(
app._( app._("sub_no", "message", locale=submission.from_user.language_code),
"sub_no",
"message",
locale=(await app.find_user(submission.from_user)).locale,
),
quote=True, quote=True,
) )
await callback.answer( await clb.answer(
text=app._("sub_no", "callback", locale=user.locale).format(fullcallback[2]), text=app._("sub_no", "callback", locale=user_locale).format(fullclb[2]),
show_alert=True, show_alert=True,
) )
@@ -187,99 +163,101 @@ async def callback_query_no(app: PyroClient, callback: CallbackQuery):
[ [
[ [
InlineKeyboardButton( InlineKeyboardButton(
text=str(app._("declined", "button", locale=user.locale)), text=str(app._("declined", "button", locale=user_locale)),
callback_data="nothing", callback_data="nothing",
) )
], ],
callback.message.reply_markup.inline_keyboard[1], clb.message.reply_markup.inline_keyboard[1],
] ]
if len(callback.message.reply_markup.inline_keyboard) > 1 if len(clb.message.reply_markup.inline_keyboard) > 1
else [ else [
[ [
InlineKeyboardButton( InlineKeyboardButton(
text=str(app._("declined", "button", locale=user.locale)), text=str(app._("declined", "button", locale=user_locale)),
callback_data="nothing", callback_data="nothing",
) )
] ]
] ]
) )
await callback.message.edit_reply_markup( await clb.message.edit_reply_markup(
reply_markup=InlineKeyboardMarkup(edited_markup) reply_markup=InlineKeyboardMarkup(edited_markup)
) )
logger.info( logger.info(
"Submission with ID '%s' rejected", app._(
fullcallback[2], "submission_rejected",
"console",
locale=app.config["locale_log"],
).format(fullclb[2]),
) )
@Client.on_callback_query(filters.regex("sub_block_[\s\S]*")) @Client.on_callback_query(filters.regex("sub_block_[\s\S]*"))
async def callback_query_block(app: PyroClient, callback: CallbackQuery): async def callback_query_block(app: PyroClient, clb: CallbackQuery):
user = await app.find_user(callback.from_user) fullclb = str(clb.data).split("_")
fullcallback = str(callback.data).split("_") user_locale = clb.from_user.language_code
await app.send_message( await app.send_message(
int(fullcallback[2]), int(fullclb[2]),
app._( app._("sub_blocked", "message"),
"sub_blocked",
"message",
locale=(await app.find_user(int(fullcallback[2]))).locale,
),
) )
await user.block() PosterUser(int(fullclb[2])).block()
await callback.answer( await clb.answer(
text=app._("sub_block", "callback", locale=user.locale).format(fullcallback[2]), text=app._("sub_block", "callback", locale=user_locale).format(fullclb[2]),
show_alert=True, show_alert=True,
) )
edited_markup = [ edited_markup = [
callback.message.reply_markup.inline_keyboard[0], clb.message.reply_markup.inline_keyboard[0],
[ [
InlineKeyboardButton( InlineKeyboardButton(
text=str(app._("sub_unblock", "button", locale=user.locale)), text=str(app._("sub_unblock", "button", locale=user_locale)),
callback_data=f"sub_unblock_{fullcallback[2]}", callback_data=f"sub_unblock_{fullclb[2]}",
) )
], ],
] ]
await callback.message.edit_reply_markup( await clb.message.edit_reply_markup(
reply_markup=InlineKeyboardMarkup(edited_markup) reply_markup=InlineKeyboardMarkup(edited_markup)
) )
logger.info("User %s has been blocked", fullcallback[2]) logger.info(
app._(
"user_blocked",
"console",
locale=app.config["locale_log"],
).format(fullclb[2]),
)
@Client.on_callback_query(filters.regex("sub_unblock_[\s\S]*")) @Client.on_callback_query(filters.regex("sub_unblock_[\s\S]*"))
async def callback_query_unblock(app: PyroClient, callback: CallbackQuery): async def callback_query_unblock(app: PyroClient, clb: CallbackQuery):
user = await app.find_user(callback.from_user) fullclb = str(clb.data).split("_")
fullcallback = str(callback.data).split("_") user_locale = clb.from_user.language_code
await app.send_message( await app.send_message(int(fullclb[2]), app._("sub_unblocked", "message"))
int(fullcallback[2]),
app._(
"sub_unblocked",
"message",
locale=(await app.find_user(int(fullcallback[2]))).locale,
),
)
await user.unblock() PosterUser(int(fullclb[2])).unblock()
await callback.answer( await clb.answer(
text=app._("sub_unblock", "callback", locale=user.locale).format( text=app._("sub_unblock", "callback", locale=user_locale).format(fullclb[2]),
fullcallback[2]
),
show_alert=True, show_alert=True,
) )
edited_markup = [ edited_markup = [
callback.message.reply_markup.inline_keyboard[0], clb.message.reply_markup.inline_keyboard[0],
[ [
InlineKeyboardButton( InlineKeyboardButton(
text=str(app._("sub_block", "button", locale=user.locale)), text=str(app._("sub_block", "button", locale=user_locale)),
callback_data=f"sub_block_{fullcallback[2]}", callback_data=f"sub_block_{fullclb[2]}",
) )
], ],
] ]
await callback.message.edit_reply_markup( await clb.message.edit_reply_markup(
reply_markup=InlineKeyboardMarkup(edited_markup) reply_markup=InlineKeyboardMarkup(edited_markup)
) )
logger.info("User %s has been unblocked", fullcallback[2]) logger.info(
app._(
"user_unblocked",
"console",
locale=app.config["locale_log"],
).format(fullclb[2]),
)

View File

@@ -1,4 +1,3 @@
import asyncio
from os import makedirs from os import makedirs
from pathlib import Path from pathlib import Path
from time import time from time import time
@@ -15,22 +14,20 @@ from modules.utils import USERS_WITH_CONTEXT
@Client.on_message( @Client.on_message(
~filters.scheduled & filters.command(["shutdown"], prefixes=["", "/"]) ~filters.scheduled & filters.command(["shutdown"], prefixes=["", "/"])
) )
async def cmd_kill(app: PyroClient, message: Message): async def cmd_kill(app: PyroClient, msg: Message):
if message.from_user.id not in app.admins: if msg.from_user.id not in app.admins:
return return
user = await app.find_user(message.from_user)
if len(USERS_WITH_CONTEXT) > 0: if len(USERS_WITH_CONTEXT) > 0:
await message.reply_text( await msg.reply_text(
app._("shutdown_confirm", "message", locale=user.locale).format( app._("shutdown_confirm", "message").format(len(USERS_WITH_CONTEXT)),
len(USERS_WITH_CONTEXT)
),
reply_markup=InlineKeyboardMarkup( reply_markup=InlineKeyboardMarkup(
[ [
[ [
InlineKeyboardButton( InlineKeyboardButton(
app._("shutdown", "button", locale=user.locale), app._(
"shutdown", "button", locale=msg.from_user.language_code
),
callback_data="shutdown", callback_data="shutdown",
) )
] ]
@@ -45,4 +42,4 @@ async def cmd_kill(app: PyroClient, message: Message):
Path(f"{app.config['locations']['cache']}/shutdown_time"), Path(f"{app.config['locations']['cache']}/shutdown_time"),
) )
asyncio.get_event_loop().create_task(app.stop()) exit()

View File

@@ -3,32 +3,22 @@ from pyrogram.client import Client
from pyrogram.types import Message from pyrogram.types import Message
from classes.pyroclient import PyroClient from classes.pyroclient import PyroClient
from modules import custom_filters from classes.user import PosterUser
@Client.on_message(~filters.scheduled & filters.command(["start"], prefixes="/"))
async def cmd_start(app: PyroClient, msg: Message):
if PosterUser(msg.from_user.id).is_blocked():
return
await msg.reply_text(app._("start", "message", locale=msg.from_user.language_code))
@Client.on_message( @Client.on_message(
custom_filters.mode_submit ~filters.scheduled & filters.command(["rules", "help"], prefixes="/")
& ~filters.scheduled
& filters.command(["start"], prefixes="/")
) )
async def cmd_start(app: PyroClient, message: Message): async def cmd_rules(app: PyroClient, msg: Message):
user = await app.find_user(message.from_user) if PosterUser(msg.from_user.id).is_blocked():
if user.banned:
return return
await message.reply_text(app._("start", "message", locale=user.locale)) await msg.reply_text(app._("rules", "message", locale=msg.from_user.language_code))
@Client.on_message(
custom_filters.mode_submit
& ~filters.scheduled
& filters.command(["rules", "help"], prefixes="/")
)
async def cmd_rules(app: PyroClient, message: Message):
user = await app.find_user(message.from_user)
if user.banned:
return
await message.reply_text(app._("rules", "message", locale=user.locale))

View File

@@ -13,12 +13,7 @@ from convopyro import listen_message
from photosapi_client.errors import UnexpectedStatus from photosapi_client.errors import UnexpectedStatus
from pyrogram import filters from pyrogram import filters
from pyrogram.client import Client from pyrogram.client import Client
from pyrogram.types import ( from pyrogram.types import Message
KeyboardButton,
Message,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
)
from ujson import loads from ujson import loads
from classes.pyroclient import PyroClient from classes.pyroclient import PyroClient
@@ -28,8 +23,6 @@ from modules.api_client import (
client, client,
photo_delete, photo_delete,
photo_upload, photo_upload,
video_delete,
video_upload,
) )
from modules.utils import USERS_WITH_CONTEXT, extract_and_save from modules.utils import USERS_WITH_CONTEXT, extract_and_save
@@ -37,34 +30,36 @@ logger = logging.getLogger(__name__)
@Client.on_message(~filters.scheduled & filters.command(["import"], prefixes=["", "/"])) @Client.on_message(~filters.scheduled & filters.command(["import"], prefixes=["", "/"]))
async def cmd_import(app: PyroClient, message: Message): async def cmd_import(app: PyroClient, msg: Message):
if message.from_user.id not in app.admins: if msg.from_user.id not in app.admins:
return return
global USERS_WITH_CONTEXT global USERS_WITH_CONTEXT
if message.from_user.id not in USERS_WITH_CONTEXT: if msg.from_user.id not in USERS_WITH_CONTEXT:
USERS_WITH_CONTEXT.append(message.from_user.id) USERS_WITH_CONTEXT.append(msg.from_user.id)
else: else:
return return
user = await app.find_user(message.from_user) await msg.reply_text(
app._("import_request", "message", locale=msg.from_user.language_code)
)
await message.reply_text(app._("import_request", "message", locale=user.locale)) answer = await listen_message(app, msg.chat.id, timeout=600)
answer = await listen_message(app, message.chat.id, timeout=600) USERS_WITH_CONTEXT.remove(msg.from_user.id)
USERS_WITH_CONTEXT.remove(message.from_user.id)
if answer is None: if answer is None:
await message.reply_text( await msg.reply_text(
app._("import_ignored", "message", locale=user.locale), app._("import_ignored", "message", locale=msg.from_user.language_code),
quote=True, quote=True,
) )
return return
if answer.text == "/cancel": if answer.text == "/cancel":
await answer.reply_text(app._("import_abort", "message", locale=user.locale)) await answer.reply_text(
app._("import_abort", "message", locale=msg.from_user.language_code)
)
return return
if answer.document is None: if answer.document is None:
@@ -72,7 +67,7 @@ async def cmd_import(app: PyroClient, message: Message):
app._( app._(
"import_invalid_media", "import_invalid_media",
"message", "message",
locale=user.locale, locale=msg.from_user.language_code,
), ),
quote=True, quote=True,
) )
@@ -80,14 +75,16 @@ async def cmd_import(app: PyroClient, message: Message):
if answer.document.mime_type != "application/zip": if answer.document.mime_type != "application/zip":
await answer.reply_text( await answer.reply_text(
app._("import_invalid_mime", "message", locale=user.locale), app._("import_invalid_mime", "message", locale=msg.from_user.language_code),
quote=True, quote=True,
) )
return return
if disk_usage(getcwd())[2] < (answer.document.file_size) * 3: if disk_usage(getcwd())[2] < (answer.document.file_size) * 3:
await message.reply_text( await msg.reply_text(
app._("import_too_big", "message", locale=user.locale).format( app._(
"import_too_big", "message", locale=msg.from_user.language_code
).format(
answer.document.file_size // (2**30), answer.document.file_size // (2**30),
disk_usage(getcwd())[2] // (2**30), disk_usage(getcwd())[2] // (2**30),
) )
@@ -107,12 +104,14 @@ async def cmd_import(app: PyroClient, message: Message):
tmp_path = Path(f"{app.config['locations']['tmp']}/{answer.document.file_id}") tmp_path = Path(f"{app.config['locations']['tmp']}/{answer.document.file_id}")
downloading = await answer.reply_text( downloading = await answer.reply_text(
app._("import_downloading", "message", locale=user.locale), app._("import_downloading", "message", locale=msg.from_user.language_code),
quote=True, quote=True,
) )
await app.download_media(answer, file_name=str(tmp_path)) await app.download_media(answer, file_name=str(tmp_path))
await downloading.edit(app._("import_unpacking", "message", locale=user.locale)) await downloading.edit(
app._("import_unpacking", "message", locale=msg.from_user.language_code)
)
try: try:
with ZipFile(tmp_path, "r") as handle: with ZipFile(tmp_path, "r") as handle:
@@ -123,23 +122,25 @@ async def cmd_import(app: PyroClient, message: Message):
for name in handle.namelist() for name in handle.namelist()
] ]
_ = await asyncio.gather(*tasks) _ = await asyncio.gather(*tasks)
except Exception as exc: except Exception as exp:
logger.error( logger.error(
"Could not import '%s' due to %s: %s", "Could not import '%s' due to %s: %s",
answer.document.file_name, answer.document.file_name,
exc, exp,
format_exc(), format_exc(),
) )
await answer.reply_text( await answer.reply_text(
app._("import_unpack_error", "message", locale=user.locale).format( app._(
exc, format_exc() "import_unpack_error", "message", locale=msg.from_user.language_code
) ).format(exp, format_exc())
) )
return return
logger.info("Downloaded '%s' - awaiting upload", answer.document.file_name) logger.info("Downloaded '%s' - awaiting upload", answer.document.file_name)
await downloading.edit(app._("import_uploading", "message", locale=user.locale)) await downloading.edit(
app._("import_uploading", "message", locale=msg.from_user.language_code)
)
remove(tmp_path) remove(tmp_path)
@@ -154,29 +155,28 @@ async def cmd_import(app: PyroClient, message: Message):
photo_bytes = BytesIO(fh.read()) photo_bytes = BytesIO(fh.read())
try: try:
# VIDEO SUPPORT IS PLANNED HERE TOO
uploaded = await photo_upload( uploaded = await photo_upload(
app.config["posting"]["api"]["album"], app.config["posting"]["api"]["album"],
client=client, client=client,
body=BodyPhotoUpload( multipart_data=BodyPhotoUpload(
File(photo_bytes, Path(filename).name, "image/jpeg") File(photo_bytes, Path(filename).name, "image/jpeg")
), ),
ignore_duplicates=app.config["submission"]["allow_duplicates"], ignore_duplicates=app.config["submission"]["allow_duplicates"],
compress=False, compress=False,
caption="queue", caption="queue",
) )
except UnexpectedStatus as exc: except UnexpectedStatus as exp:
logger.error( logger.error(
"Could not upload '%s' from '%s': %s", "Could not upload '%s' from '%s': %s",
filename, filename,
Path(f"{app.config['locations']['tmp']}/{tmp_dir}"), Path(f"{app.config['locations']['tmp']}/{tmp_dir}"),
exc, exp,
) )
await message.reply_text( await msg.reply_text(
app._( app._(
"import_upload_error_other", "import_upload_error_other",
"message", "message",
locale=user.locale, locale=msg.from_user.language_code,
).format(path.basename(filename)), ).format(path.basename(filename)),
disable_notification=True, disable_notification=True,
) )
@@ -193,20 +193,20 @@ async def cmd_import(app: PyroClient, message: Message):
) )
if len(uploaded_dict["duplicates"]) > 0: if len(uploaded_dict["duplicates"]) > 0:
await message.reply_text( await msg.reply_text(
app._( app._(
"import_upload_error_duplicate", "import_upload_error_duplicate",
"message", "message",
locale=user.locale, locale=msg.from_user.language_code,
).format(path.basename(filename)), ).format(path.basename(filename)),
disable_notification=True, disable_notification=True,
) )
else: else:
await message.reply_text( await msg.reply_text(
app._( app._(
"import_upload_error_other", "import_upload_error_other",
"message", "message",
locale=user.locale, locale=msg.from_user.language_code,
).format(path.basename(filename)), ).format(path.basename(filename)),
disable_notification=True, disable_notification=True,
) )
@@ -227,7 +227,7 @@ async def cmd_import(app: PyroClient, message: Message):
rmtree(Path(f"{app.config['locations']['tmp']}/{tmp_dir}"), ignore_errors=True) rmtree(Path(f"{app.config['locations']['tmp']}/{tmp_dir}"), ignore_errors=True)
await answer.reply_text( await answer.reply_text(
app._("import_finished", "message", locale=user.locale), app._("import_finished", "message", locale=msg.from_user.language_code),
quote=True, quote=True,
) )
@@ -235,126 +235,69 @@ async def cmd_import(app: PyroClient, message: Message):
@Client.on_message(~filters.scheduled & filters.command(["export"], prefixes=["", "/"])) @Client.on_message(~filters.scheduled & filters.command(["export"], prefixes=["", "/"]))
async def cmd_export(app: PyroClient, message: Message): async def cmd_export(app: PyroClient, msg: Message):
if message.from_user.id not in app.admins: if msg.from_user.id not in app.admins:
return return
@Client.on_message(~filters.scheduled & filters.command(["remove"], prefixes=["", "/"])) @Client.on_message(~filters.scheduled & filters.command(["remove"], prefixes=["", "/"]))
async def cmd_remove(app: PyroClient, message: Message): async def cmd_remove(app: PyroClient, msg: Message):
if message.from_user.id not in app.admins: if msg.from_user.id not in app.admins:
return return
global USERS_WITH_CONTEXT global USERS_WITH_CONTEXT
if message.from_user.id not in USERS_WITH_CONTEXT: if msg.from_user.id not in USERS_WITH_CONTEXT:
USERS_WITH_CONTEXT.append(message.from_user.id) USERS_WITH_CONTEXT.append(msg.from_user.id)
else: else:
return return
user = await app.find_user(message.from_user) await msg.reply_text(
app._("remove_request", "message", locale=msg.from_user.language_code)
await message.reply_text(app._("remove_request", "message", locale=user.locale))
answer_id = await app.listen.Message(
filters.text & ~filters.me, id=filters.user(message.from_user.id), timeout=600
) )
USERS_WITH_CONTEXT.remove(message.from_user.id) answer = await listen_message(app, msg.chat.id, timeout=600)
if answer_id is None: USERS_WITH_CONTEXT.remove(msg.from_user.id)
await message.reply_text(
app._("remove_ignored", "message", locale=user.locale), if answer is None:
await msg.reply_text(
app._("remove_ignored", "message", locale=msg.from_user.language_code),
quote=True, quote=True,
) )
return return
if answer_id.text == "/cancel": if answer.text == "/cancel":
await answer_id.reply_text(app._("remove_abort", "message", locale=user.locale)) await answer.reply_text(
return app._("remove_abort", "message", locale=msg.from_user.language_code)
await message.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(message.from_user.id)
answer_kind = await app.listen.Message(
filters.text & ~filters.me, id=filters.user(message.from_user.id), timeout=600
)
USERS_WITH_CONTEXT.remove(message.from_user.id)
if answer_kind is None:
await message.reply_text(
app._("remove_ignored", "message", locale=user.locale),
quote=True,
reply_markup=ReplyKeyboardRemove(),
) )
return return
if answer_kind.text == "/cancel": response = await photo_delete(id=answer.text, client=client)
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"): if response:
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( logger.info(
"Removed %s '%s' by request of user %s", "Removed '%s' by request of user %s", answer.text, answer.from_user.id
answer_kind.text,
answer_id.text,
answer_id.from_user.id,
) )
await answer_kind.reply_text( await answer.reply_text(
app._("remove_success", "message", locale=user.locale).format( app._(
answer_id.text "remove_success", "message", locale=msg.from_user.language_code
), ).format(answer.text)
reply_markup=ReplyKeyboardRemove(),
) )
else: else:
logger.warning( logger.warning(
"Could not remove %s '%s' by request of user %s", "Could not remove '%s' by request of user %s",
answer_kind.text, answer.text,
answer_id.text, answer.from_user.id,
answer_id.from_user.id,
) )
await answer_kind.reply_text( await answer.reply_text(
app._("remove_failure", "message", locale=user.locale).format( app._(
answer_id.text "remove_failure", "message", locale=msg.from_user.language_code
), ).format(answer.text)
reply_markup=ReplyKeyboardRemove(),
) )
@Client.on_message(~filters.scheduled & filters.command(["purge"], prefixes=["", "/"])) @Client.on_message(~filters.scheduled & filters.command(["purge"], prefixes=["", "/"]))
async def cmd_purge(app: PyroClient, message: Message): async def cmd_purge(app: PyroClient, msg: Message):
if message.from_user.id not in app.admins: if msg.from_user.id not in app.admins:
return return

View File

@@ -1,43 +1,41 @@
from libbot import sync
from pyrogram import filters
from pyrogram.client import Client from pyrogram.client import Client
from pyrogram import filters
from pyrogram.types import Message, User from pyrogram.types import Message, User
from libbot import sync
from classes.pyroclient import PyroClient from classes.pyroclient import PyroClient
from modules import custom_filters
@Client.on_message( @Client.on_message(
custom_filters.mode_post ~filters.scheduled
& ~filters.scheduled
& filters.chat(sync.config_get("comments", "posting")) & filters.chat(sync.config_get("comments", "posting"))
& filters.reply & filters.reply
& filters.command(["report"], prefixes=["", "/"]) & filters.command(["report"], prefixes=["", "/"])
) )
async def command_report(app: PyroClient, message: Message): async def command_report(app: PyroClient, msg: Message):
if ( if msg.reply_to_message.forward_from_chat.id == app.config["posting"]["channel"]:
message.reply_to_message.forward_from_chat.id await msg.reply_text(
!= app.config["posting"]["channel"]
):
return
user = await app.find_user(message.from_user)
await message.reply_text(
app._( app._(
"report_sent", "report_sent",
"message", "message",
locale=user.locale if message.from_user is not None else None, locale=msg.from_user.language_code
if msg.from_user is not None
else None,
) )
) )
report_sent = await message.reply_to_message.forward(app.owner) print(msg)
sender = message.from_user if message.from_user is not None else message.sender_chat
sender_name = sender.first_name if isinstance(sender, User) else sender.title 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
)
# ACTION NEEDED
# Name and username are somehow None
await report_sent.reply_text( await report_sent.reply_text(
app._("report_received", "message", locale=user.locale).format( app._("report_received", "message").format(
sender_name, sender.username, sender.id sender_name, sender.username, sender.id
), ),
quote=True, quote=True,

View File

@@ -13,134 +13,126 @@ from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
from classes.enums.submission_types import SubmissionType from classes.enums.submission_types import SubmissionType
from classes.exceptions import SubmissionDuplicatesError, SubmissionUnsupportedError from classes.exceptions import SubmissionDuplicatesError, SubmissionUnsupportedError
from classes.pyroclient import PyroClient from classes.pyroclient import PyroClient
from modules import custom_filters from classes.user import PosterUser
from modules.database import col_submitted from modules.database import col_banned, col_submitted
from modules.utils import USERS_WITH_CONTEXT from modules.utils import USERS_WITH_CONTEXT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@Client.on_message( @Client.on_message(
custom_filters.mode_submit & ~filters.scheduled & filters.private & filters.photo ~filters.scheduled & filters.private & filters.photo
| filters.video | filters.video
# | filters.animation # | filters.animation
| filters.document | filters.document
) )
async def get_submission(app: PyroClient, message: Message): async def get_submission(app: PyroClient, msg: Message):
global USERS_WITH_CONTEXT global USERS_WITH_CONTEXT
if not hasattr(message.from_user, "id"): if not hasattr(msg.from_user, "id"):
return return
if message.from_user.id in USERS_WITH_CONTEXT: if msg.from_user.id in USERS_WITH_CONTEXT:
return return
user = await app.find_user(message.from_user)
user_owner = await app.find_user(app.owner)
try: try:
if user.banned: if col_banned.find_one({"user": msg.from_user.id}) is not None:
return return
await app.send_chat_action(message.chat.id, ChatAction.TYPING) await app.send_chat_action(msg.chat.id, ChatAction.TYPING)
user_locale = msg.from_user.language_code
save_tmp = True save_tmp = True
contents = None contents = None
if await user.is_limited(): if PosterUser(msg.from_user.id).is_limited():
await message.reply_text( await msg.reply_text(
app._("sub_cooldown", "message", locale=user.locale).format( app._("sub_cooldown", "message", locale=user_locale).format(
app.config["submission"]["timeout"] str(app.config["submission"]["timeout"])
) )
) )
return return
if message.document is not None: if msg.document is not None:
logger.info( logger.info(
"User %s is trying to submit a file of type '%s' with name '%s' and size of %s MB", "User %s is trying to submit a file of type '%s' with name '%s' and size of %s MB",
message.from_user.id, msg.from_user.id,
message.document.mime_type, msg.document.mime_type,
message.document.file_name, msg.document.file_name,
message.document.file_size / 1024 / 1024, msg.document.file_size / 1024 / 1024,
) )
if message.document.mime_type not in app.config["submission"]["mime_types"]: if msg.document.mime_type not in app.config["submission"]["mime_types"]:
await message.reply_text( await msg.reply_text(
app._("mime_not_allowed", "message", locale=user.locale).format( app._("mime_not_allowed", "message", locale=user_locale).format(
", ".join(app.config["submission"]["mime_types"]) ", ".join(app.config["submission"]["mime_types"])
), ),
quote=True, quote=True,
) )
return return
if message.document.file_size > app.config["submission"]["file_size"]: if msg.document.file_size > app.config["submission"]["file_size"]:
await message.reply_text( await msg.reply_text(
app._("document_too_large", "message", locale=user.locale).format( app._("document_too_large", "message", locale=user_locale).format(
app.config["submission"]["file_size"] / 1024 / 1024 str(app.config["submission"]["file_size"] / 1024 / 1024)
), ),
quote=True, quote=True,
) )
return return
if message.document.file_size > app.config["submission"]["tmp_size"]: if msg.document.file_size > app.config["submission"]["tmp_size"]:
save_tmp = False save_tmp = False
contents = ( contents = (
message.document.file_id, msg.document.file_id,
SubmissionType.DOCUMENT, SubmissionType.DOCUMENT,
) # , message.document.file_name ) # , msg.document.file_name
if message.video is not None: if msg.video is not None:
logger.info( logger.info(
"User %s is trying to submit a video with name '%s' and size of %s MB", "User %s is trying to submit a video with name '%s' and size of %s MB",
message.from_user.id, msg.from_user.id,
message.video.file_name, msg.video.file_name,
message.video.file_size / 1024 / 1024, msg.video.file_size / 1024 / 1024,
) )
if message.video.file_size > app.config["submission"]["file_size"]: if msg.video.file_size > app.config["submission"]["file_size"]:
await message.reply_text( await msg.reply_text(
app._("document_too_large", "message", locale=user.locale).format( app._("document_too_large", "message", locale=user_locale).format(
app.config["submission"]["file_size"] / 1024 / 1024 str(app.config["submission"]["file_size"] / 1024 / 1024)
), ),
quote=True, quote=True,
) )
return return
if message.video.file_size > app.config["submission"]["tmp_size"]: if msg.video.file_size > app.config["submission"]["tmp_size"]:
save_tmp = False save_tmp = False
contents = ( contents = msg.video.file_id, SubmissionType.VIDEO # , msg.video.file_name
message.video.file_id,
SubmissionType.VIDEO,
) # , message.video.file_name
# if message.animation is not None: # if msg.animation is not None:
# logger.info( # logger.info(
# "User %s is trying to submit an animation with name '%s' and size of %s MB", # "User %s is trying to submit an animation with name '%s' and size of %s MB",
# message.from_user.id, # msg.from_user.id,
# message.animation.file_name, # msg.animation.file_name,
# message.animation.file_size / 1024 / 1024, # msg.animation.file_size / 1024 / 1024,
# ) # )
# if message.animation.file_size > app.config["submission"]["file_size"]: # if msg.animation.file_size > app.config["submission"]["file_size"]:
# await message.reply_text( # await msg.reply_text(
# app._("document_too_large", "message", locale=user.locale).format( # app._("document_too_large", "message", locale=user_locale).format(
# str(app.config["submission"]["file_size"] / 1024 / 1024) # str(app.config["submission"]["file_size"] / 1024 / 1024)
# ), # ),
# quote=True, # quote=True,
# ) # )
# return # return
# if message.animation.file_size > app.config["submission"]["tmp_size"]: # if msg.animation.file_size > app.config["submission"]["tmp_size"]:
# save_tmp = False # save_tmp = False
# contents = ( # contents = (
# message.animation.file_id, # msg.animation.file_id,
# SubmissionType.ANIMATION, # SubmissionType.ANIMATION,
# ) # , message.animation.file_name # ) # , msg.animation.file_name
if message.photo is not None: if msg.photo is not None:
logger.info( logger.info(
"User %s is trying to submit a photo with ID '%s' and size of %s MB", "User %s is trying to submit a photo with ID '%s' and size of %s MB",
message.from_user.id, msg.from_user.id,
message.photo.file_id, msg.photo.file_id,
message.photo.file_size / 1024 / 1024, msg.photo.file_size / 1024 / 1024,
) )
contents = ( contents = msg.photo.file_id, SubmissionType.PHOTO # , "please_generate"
message.photo.file_id,
SubmissionType.PHOTO,
) # , "please_generate"
if contents is None: if contents is None:
return return
@@ -154,54 +146,50 @@ async def get_submission(app: PyroClient, message: Message):
exist_ok=True, exist_ok=True,
) )
downloaded = await app.download_media( downloaded = await app.download_media(
message, msg,
str(Path(f"{app.config['locations']['data']}/submissions/{tmp_id}")) str(Path(f"{app.config['locations']['data']}/submissions/{tmp_id}"))
+ sep, + sep,
) )
inserted = await col_submitted.insert_one( inserted = col_submitted.insert_one(
{ {
"user": message.from_user.id, "user": msg.from_user.id,
"date": datetime.now(), "date": datetime.now(),
"done": False, "done": False,
"type": contents[1].value, "type": contents[1].value,
"temp": {"uuid": tmp_id, "file": path.basename(str(downloaded))}, "temp": {"uuid": tmp_id, "file": path.basename(str(downloaded))},
"telegram": {"msg_id": message.id, "file_id": contents[0]}, "telegram": {"msg_id": msg.id, "file_id": contents[0]},
"caption": str(message.caption) "caption": str(msg.caption) if msg.caption is not None else None,
if message.caption is not None
else None,
} }
) )
else: else:
inserted = await col_submitted.insert_one( inserted = col_submitted.insert_one(
{ {
"user": message.from_user.id, "user": msg.from_user.id,
"date": datetime.now(), "date": datetime.now(),
"done": False, "done": False,
"type": contents[1].value, "type": contents[1].value,
"temp": {"uuid": None, "file": None}, "temp": {"uuid": None, "file": None},
"telegram": {"msg_id": message.id, "file_id": contents[0]}, "telegram": {"msg_id": msg.id, "file_id": contents[0]},
"caption": str(message.caption) "caption": str(msg.caption) if msg.caption is not None else None,
if message.caption is not None
else None,
} }
) )
buttons = [ buttons = [
[ [
InlineKeyboardButton( InlineKeyboardButton(
text=app._("sub_yes", "button", locale=user_owner.locale), text=app._("sub_yes", "button"),
callback_data=f"sub_yes_{str(inserted.inserted_id)}", callback_data=f"sub_yes_{str(inserted.inserted_id)}",
) )
] ]
] ]
if message.caption is not None: if msg.caption is not None:
caption = str(message.caption) caption = str(msg.caption)
buttons[0].append( buttons[0].append(
InlineKeyboardButton( InlineKeyboardButton(
text=app._("sub_yes_caption", "button", locale=user_owner.locale), text=app._("sub_yes_caption", "button"),
callback_data=f"sub_yes_{str(inserted.inserted_id)}_caption", callback_data=f"sub_yes_{str(inserted.inserted_id)}_caption",
) )
) )
@@ -210,114 +198,112 @@ async def get_submission(app: PyroClient, message: Message):
buttons[0].append( buttons[0].append(
InlineKeyboardButton( InlineKeyboardButton(
text=app._("sub_no", "button", locale=user_owner.locale), text=app._("sub_no", "button"),
callback_data=f"sub_no_{str(inserted.inserted_id)}", callback_data=f"sub_no_{str(inserted.inserted_id)}",
) )
) )
caption += app._("sub_by", "message", locale=user_owner.locale) caption += app._("sub_by", "message")
if message.from_user.first_name is not None: if msg.from_user.first_name is not None:
caption += f" {message.from_user.first_name}" caption += f" {msg.from_user.first_name}"
if message.from_user.last_name is not None: if msg.from_user.last_name is not None:
caption += f" {message.from_user.last_name}" caption += f" {msg.from_user.last_name}"
if message.from_user.username is not None: if msg.from_user.username is not None:
caption += f" (@{message.from_user.username})" caption += f" (@{msg.from_user.username})"
if message.from_user.phone_number is not None: if msg.from_user.phone_number is not None:
caption += f" ({message.from_user.phone_number})" caption += f" ({msg.from_user.phone_number})"
if ( if (
message.from_user.id in app.admins msg.from_user.id in app.admins
and app.config["submission"]["require_confirmation"]["admins"] is False and app.config["submission"]["require_confirmation"]["admins"] is False
): ):
try: try:
submitted = await app.submit_media(str(inserted.inserted_id)) submitted = await app.submit_media(str(inserted.inserted_id))
await message.reply_text( await msg.reply_text(
app._("sub_yes_auto", "message", locale=user.locale), app._("sub_yes_auto", "message", locale=user_locale),
disable_notification=True, disable_notification=True,
quote=True, quote=True,
) )
if app.config["submission"]["send_uploaded_id"]: if app.config["submission"]["send_uploaded_id"]:
caption += f"\n\nID: `{submitted[1]}`" caption += f"\n\nID: `{submitted[1]}`"
await message.copy( await msg.copy(app.owner, caption=caption, disable_notification=True)
app.owner, caption=caption, disable_notification=True
)
return return
except SubmissionUnsupportedError: except SubmissionUnsupportedError:
await message.reply_text( await msg.reply_text(
app._("mime_not_allowed", "message", locale=user.locale).format( app._("mime_not_allowed", "message", locale=user_locale).format(
", ".join(app.config["submission"]["mime_types"]), quote=True ", ".join(app.config["submission"]["mime_types"]), quote=True
), ),
quote=True, quote=True,
) )
return return
except SubmissionDuplicatesError as exc: except SubmissionDuplicatesError as exp:
await message.reply_text( await msg.reply_text(
app._( app._(
"sub_media_duplicates_list", "message", locale=user.locale "sub_media_duplicates_list", "message", locale=user_locale
).format("\n".join(exc.duplicates)), ).format("\n".join(exp.duplicates)),
quote=True, quote=True,
) )
return return
except Exception as exc: except Exception as exp:
await message.reply_text(exc, quote=True) await msg.reply_text(format_exc(), quote=True)
return return
elif ( elif (
message.from_user.id not in app.admins msg.from_user.id not in app.admins
and app.config["submission"]["require_confirmation"]["users"] is False and app.config["submission"]["require_confirmation"]["users"] is False
): ):
try: try:
submitted = await app.submit_photo(str(inserted.inserted_id)) submitted = await app.submit_photo(str(inserted.inserted_id))
await message.reply_text( await msg.reply_text(
app._("sub_yes_auto", "message", locale=user.locale), app._("sub_yes_auto", "message", locale=user_locale),
disable_notification=True, disable_notification=True,
quote=True, quote=True,
) )
if app.config["submission"]["send_uploaded_id"]: if app.config["submission"]["send_uploaded_id"]:
caption += f"\n\nID: `{submitted[1]}`" caption += f"\n\nID: `{submitted[1]}`"
await message.copy(app.owner, caption=caption) await msg.copy(app.owner, caption=caption)
return return
except SubmissionUnsupportedError: except SubmissionUnsupportedError:
await message.reply_text( await msg.reply_text(
app._("mime_not_allowed", "message", locale=user.locale).format( app._("mime_not_allowed", "message", locale=user_locale).format(
", ".join(app.config["submission"]["mime_types"]), quote=True ", ".join(app.config["submission"]["mime_types"]), quote=True
) )
) )
return return
except SubmissionDuplicatesError as exc: except SubmissionDuplicatesError as exp:
await message.reply_text( await msg.reply_text(
app._("sub_dup", "message", locale=user.locale), quote=True app._("sub_dup", "message", locale=user_locale), quote=True
) )
return return
except Exception as exc: except Exception as exp:
await app.send_message( await app.send_message(
app.owner, app.owner,
app._( app._("sub_error_admin", "message").format(
"sub_error_admin", "message", locale=user_owner.locale msg.from_user.id, format_exc()
).format(message.from_user.id, format_exc()), ),
) )
await message.reply_text("sub_error", quote=True) await msg.reply_text("sub_error", quote=True)
return return
if message.from_user.id not in app.admins: if msg.from_user.id not in app.admins:
buttons += [ buttons += [
[ [
InlineKeyboardButton( InlineKeyboardButton(
text=app._("sub_block", "button", locale=user_owner.locale), text=app._("sub_block", "button"),
callback_data=f"sub_block_{message.from_user.id}", callback_data=f"sub_block_{msg.from_user.id}",
) )
] ]
] ]
await user.update_cooldown() PosterUser(msg.from_user.id).limit()
if message.from_user.id != app.owner: if msg.from_user.id != app.owner:
await message.reply_text( await msg.reply_text(
app._("sub_sent", "message", locale=user.locale), app._("sub_sent", "message", locale=user_locale),
disable_notification=True, disable_notification=True,
quote=True, quote=True,
) )
await message.copy( await msg.copy(
app.owner, caption=caption, reply_markup=InlineKeyboardMarkup(buttons) app.owner, caption=caption, reply_markup=InlineKeyboardMarkup(buttons)
) )

View File

@@ -1,42 +0,0 @@
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,
)

View File

@@ -8,6 +8,6 @@ from classes.pyroclient import PyroClient
@Client.on_message( @Client.on_message(
~filters.scheduled & filters.private & filters.command(["remove_commands"], prefixes=["/"]) # type: ignore ~filters.scheduled & filters.private & filters.command(["remove_commands"], prefixes=["/"]) # type: ignore
) )
async def command_remove_commands(app: PyroClient, message: Message): async def command_remove_commands(app: PyroClient, msg: Message):
await message.reply_text("Okay.") await msg.reply_text("Okay.")
await app.remove_commands(command_sets=await app.collect_commands()) await app.remove_commands(command_sets=await app.collect_commands())

View File

@@ -1,11 +1,14 @@
aiohttp~=3.10.2 aiohttp~=3.8.4
async_pymongo==0.1.6 black~=23.3.0
convopyro==0.5 convopyro==0.5
pillow~=10.4.0 pillow~=9.4.0
pykeyboard==0.1.7 psutil~=5.9.4
pymongo~=4.4.0
pyrogram==2.0.106
python_dateutil==2.8.2
pytimeparse~=1.1.8 pytimeparse~=1.1.8
tgcrypto==1.2.5 tgcrypto==1.2.5
#uvloop==0.19.0 uvloop==0.17.0
--extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple --extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple
libbot[speed,pyrogram]==3.2.3 libbot[speed,pyrogram]==1.5
photosapi_client==0.6.0 photosapi_client==0.5.0

View File

@@ -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 main.py python poster.py

View File

@@ -3,4 +3,4 @@
# You can cd to your directory here, if you want # You can cd to your directory here, if you want
# cd /home/user/TelegramPoster # cd /home/user/TelegramPoster
python main.py python poster.py