29 Commits

Author SHA1 Message Date
32e6c4e96f Updated to 0.3 2023-07-06 15:24:46 +03:00
8c27fb7c37 Small fixes (#32)
* `bot.max_concurrent_transmissions` reduced to only 1
* Fixed using UnauthorizedClient when not needed

Co-authored-by: profitroll <vozhd.kk@gmail.com>
Reviewed-on: #32
2023-07-06 15:21:21 +03:00
82467518da Merge branch 'master' of https://git.end-play.xyz/profitroll/TelegramPoster 2023-07-03 12:47:29 +02:00
c0085b8000 Merge branch 'dev' 2023-07-03 12:43:43 +02:00
a7e79eb254 Updated README 2023-07-03 12:34:55 +02:00
dc774262f8 Locale for console is gone for good 2023-07-03 11:42:28 +02:00
987f642578 CLI is back and updated 2023-07-03 11:27:15 +02:00
f7df4d8ddc Bump libbot to 1.8 2023-07-03 11:04:39 +02:00
c2619a1370 Update dependency pillow to v10 (#29)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [pillow](https://python-pillow.org) ([source](https://github.com/python-pillow/Pillow), [changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)) | major | `~=9.4.0` -> `~=10.0.0` |

---

### Release Notes

<details>
<summary>python-pillow/Pillow</summary>

### [`v10.0.0`](https://github.com/python-pillow/Pillow/blob/HEAD/CHANGES.rst#&#8203;1000-2023-07-01)

[Compare Source](https://github.com/python-pillow/Pillow/compare/9.5.0...10.0.0)

-   Fixed deallocating mask images [#&#8203;7246](https://github.com/python-pillow/Pillow/issues/7246)
    \[radarhere]

-   Added ImageFont.MAX_STRING_LENGTH [#&#8203;7244](https://github.com/python-pillow/Pillow/issues/7244)
    \[radarhere, hugovk]

-   Fix Windows build with pyproject.toml [#&#8203;7230](https://github.com/python-pillow/Pillow/issues/7230)
    \[hugovk, nulano, radarhere]

-   Do not close provided file handles with libtiff [#&#8203;7199](https://github.com/python-pillow/Pillow/issues/7199)
    \[radarhere]

-   Convert to HSV if mode is HSV in getcolor() [#&#8203;7226](https://github.com/python-pillow/Pillow/issues/7226)
    \[radarhere]

-   Added alpha_only argument to getbbox() [#&#8203;7123](https://github.com/python-pillow/Pillow/issues/7123)
    \[radarhere. hugovk]

-   Prioritise speed in *repr_png* [#&#8203;7242](https://github.com/python-pillow/Pillow/issues/7242)
    \[radarhere]

-   Do not use CFFI access by default on PyPy [#&#8203;7236](https://github.com/python-pillow/Pillow/issues/7236)
    \[radarhere]

-   Limit size even if one dimension is zero in decompression bomb check [#&#8203;7235](https://github.com/python-pillow/Pillow/issues/7235)
    \[radarhere]

-   Use --config-settings instead of deprecated --global-option [#&#8203;7171](https://github.com/python-pillow/Pillow/issues/7171)
    \[radarhere]

-   Better C integer definitions [#&#8203;6645](https://github.com/python-pillow/Pillow/issues/6645)
    \[Yay295, hugovk]

-   Fixed finding dependencies on Cygwin [#&#8203;7175](https://github.com/python-pillow/Pillow/issues/7175)
    \[radarhere]

-   Changed grabclipboard() to use PNG instead of JPG compression on macOS [#&#8203;7219](https://github.com/python-pillow/Pillow/issues/7219)
    \[abey79, radarhere]

-   Added in_place argument to ImageOps.exif_transpose() [#&#8203;7092](https://github.com/python-pillow/Pillow/issues/7092)
    \[radarhere]

-   Fixed calling putpalette() on L and LA images before load() [#&#8203;7187](https://github.com/python-pillow/Pillow/issues/7187)
    \[radarhere]

-   Fixed saving TIFF multiframe images with LONG8 tag types [#&#8203;7078](https://github.com/python-pillow/Pillow/issues/7078)
    \[radarhere]

-   Fixed combining single duration across duplicate APNG frames [#&#8203;7146](https://github.com/python-pillow/Pillow/issues/7146)
    \[radarhere]

-   Remove temporary file when error is raised [#&#8203;7148](https://github.com/python-pillow/Pillow/issues/7148)
    \[radarhere]

-   Do not use temporary file when grabbing clipboard on Linux [#&#8203;7200](https://github.com/python-pillow/Pillow/issues/7200)
    \[radarhere]

-   If the clipboard fails to open on Windows, wait and try again [#&#8203;7141](https://github.com/python-pillow/Pillow/issues/7141)
    \[radarhere]

-   Fixed saving multiple 1 mode frames to GIF [#&#8203;7181](https://github.com/python-pillow/Pillow/issues/7181)
    \[radarhere]

-   Replaced absolute PIL import with relative import [#&#8203;7173](https://github.com/python-pillow/Pillow/issues/7173)
    \[radarhere]

-   Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 [#&#8203;7192](https://github.com/python-pillow/Pillow/issues/7192)
    \[radarhere]

-   Improved wl-paste mimetype handling in ImageGrab [#&#8203;7094](https://github.com/python-pillow/Pillow/issues/7094)
    \[rrcgat, radarhere]

-   Added *repr_jpeg*() for IPython display_jpeg [#&#8203;7135](https://github.com/python-pillow/Pillow/issues/7135)
    \[n3011, radarhere, nulano]

-   Use "/sbin/ldconfig" if ldconfig is not found [#&#8203;7068](https://github.com/python-pillow/Pillow/issues/7068)
    \[radarhere]

-   Prefer screenshots using XCB over gnome-screenshot [#&#8203;7143](https://github.com/python-pillow/Pillow/issues/7143)
    \[nulano, radarhere]

-   Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions [#&#8203;7151](https://github.com/python-pillow/Pillow/issues/7151)
    \[radarhere]

-   Support reading signed 8-bit TIFF images [#&#8203;7111](https://github.com/python-pillow/Pillow/issues/7111)
    \[radarhere]

-   Added width argument to ImageDraw regular_polygon [#&#8203;7132](https://github.com/python-pillow/Pillow/issues/7132)
    \[radarhere]

-   Support I mode for ImageFilter.BuiltinFilter [#&#8203;7108](https://github.com/python-pillow/Pillow/issues/7108)
    \[radarhere]

-   Raise error from stderr of Linux ImageGrab.grabclipboard() command [#&#8203;7112](https://github.com/python-pillow/Pillow/issues/7112)
    \[radarhere]

-   Added unpacker from I;16B to I;16 [#&#8203;7125](https://github.com/python-pillow/Pillow/issues/7125)
    \[radarhere]

-   Support float font sizes [#&#8203;7107](https://github.com/python-pillow/Pillow/issues/7107)
    \[radarhere]

-   Use later value for duplicate xref entries in PdfParser [#&#8203;7102](https://github.com/python-pillow/Pillow/issues/7102)
    \[radarhere]

-   Load before getting size in **getstate** [#&#8203;7105](https://github.com/python-pillow/Pillow/issues/7105)
    \[bigcat88, radarhere]

-   Fixed type handling for include and lib directories [#&#8203;7069](https://github.com/python-pillow/Pillow/issues/7069)
    \[adisbladis, radarhere]

-   Remove deprecations for Pillow 10.0.0 [#&#8203;7059](https://github.com/python-pillow/Pillow/issues/7059), [#&#8203;7080](https://github.com/python-pillow/Pillow/issues/7080)
    \[hugovk, radarhere]

-   Drop support for soon-EOL Python 3.7 [#&#8203;7058](https://github.com/python-pillow/Pillow/issues/7058)
    \[hugovk, radarhere]

### [`v9.5.0`](https://github.com/python-pillow/Pillow/blob/HEAD/CHANGES.rst#&#8203;950-2023-04-01)

[Compare Source](https://github.com/python-pillow/Pillow/compare/9.4.0...9.5.0)

-   Added ImageSourceData to TAGS_V2 [#&#8203;7053](https://github.com/python-pillow/Pillow/issues/7053)
    \[radarhere]

-   Clear PPM half token after use [#&#8203;7052](https://github.com/python-pillow/Pillow/issues/7052)
    \[radarhere]

-   Removed absolute path to ldconfig [#&#8203;7044](https://github.com/python-pillow/Pillow/issues/7044)
    \[radarhere]

-   Support custom comments and PLT markers when saving JPEG2000 images [#&#8203;6903](https://github.com/python-pillow/Pillow/issues/6903)
    \[joshware, radarhere, hugovk]

-   Load before getting size in **array_interface** [#&#8203;7034](https://github.com/python-pillow/Pillow/issues/7034)
    \[radarhere]

-   Support creating BGR;15, BGR;16 and BGR;24 images, but drop support for BGR;32 [#&#8203;7010](https://github.com/python-pillow/Pillow/issues/7010)
    \[radarhere]

-   Consider transparency when applying APNG blend mask [#&#8203;7018](https://github.com/python-pillow/Pillow/issues/7018)
    \[radarhere]

-   Round duration when saving animated WebP images [#&#8203;6996](https://github.com/python-pillow/Pillow/issues/6996)
    \[radarhere]

-   Added reading of JPEG2000 comments [#&#8203;6909](https://github.com/python-pillow/Pillow/issues/6909)
    \[radarhere]

-   Decrement reference count [#&#8203;7003](https://github.com/python-pillow/Pillow/issues/7003)
    \[radarhere, nulano]

-   Allow libtiff_support_custom_tags to be missing [#&#8203;7020](https://github.com/python-pillow/Pillow/issues/7020)
    \[radarhere]

-   Improved I;16N support [#&#8203;6834](https://github.com/python-pillow/Pillow/issues/6834)
    \[radarhere]

-   Added QOI reading [#&#8203;6852](https://github.com/python-pillow/Pillow/issues/6852)
    \[radarhere, hugovk]

-   Added saving RGBA images as PDFs [#&#8203;6925](https://github.com/python-pillow/Pillow/issues/6925)
    \[radarhere]

-   Do not raise an error if os.environ does not contain PATH [#&#8203;6935](https://github.com/python-pillow/Pillow/issues/6935)
    \[radarhere, hugovk]

-   Close OleFileIO instance when closing or exiting FPX or MIC [#&#8203;7005](https://github.com/python-pillow/Pillow/issues/7005)
    \[radarhere]

-   Added **int** to IFDRational for Python >= 3.11 [#&#8203;6998](https://github.com/python-pillow/Pillow/issues/6998)
    \[radarhere]

-   Added memoryview support to Dib.frombytes() [#&#8203;6988](https://github.com/python-pillow/Pillow/issues/6988)
    \[radarhere, nulano]

-   Close file pointer copy in the libtiff encoder if still open [#&#8203;6986](https://github.com/python-pillow/Pillow/issues/6986)
    \[fcarron, radarhere]

-   Raise an error if ImageDraw co-ordinates are incorrectly ordered [#&#8203;6978](https://github.com/python-pillow/Pillow/issues/6978)
    \[radarhere]

-   Added "corners" argument to ImageDraw rounded_rectangle() [#&#8203;6954](https://github.com/python-pillow/Pillow/issues/6954)
    \[radarhere]

-   Added memoryview support to frombytes() [#&#8203;6974](https://github.com/python-pillow/Pillow/issues/6974)
    \[radarhere]

-   Allow comments in FITS images [#&#8203;6973](https://github.com/python-pillow/Pillow/issues/6973)
    \[radarhere]

-   Support saving PDF with different X and Y resolutions [#&#8203;6961](https://github.com/python-pillow/Pillow/issues/6961)
    \[jvanderneutstulen, radarhere, hugovk]

-   Fixed writing int as UNDEFINED tag [#&#8203;6950](https://github.com/python-pillow/Pillow/issues/6950)
    \[radarhere]

-   Raise an error if EXIF data is too long when saving JPEG [#&#8203;6939](https://github.com/python-pillow/Pillow/issues/6939)
    \[radarhere]

-   Handle more than one directory returned by pkg-config [#&#8203;6896](https://github.com/python-pillow/Pillow/issues/6896)
    \[sebastic, radarhere]

-   Do not retry past formats when loading all formats for the first time [#&#8203;6902](https://github.com/python-pillow/Pillow/issues/6902)
    \[radarhere]

-   Do not retry specified formats if they failed when opening [#&#8203;6893](https://github.com/python-pillow/Pillow/issues/6893)
    \[radarhere]

-   Do not unintentionally load TIFF format at first [#&#8203;6892](https://github.com/python-pillow/Pillow/issues/6892)
    \[radarhere]

-   Stop reading when EPS line becomes too long [#&#8203;6897](https://github.com/python-pillow/Pillow/issues/6897)
    \[radarhere]

-   Allow writing IFDRational to BYTE tag [#&#8203;6890](https://github.com/python-pillow/Pillow/issues/6890)
    \[radarhere]

-   Raise ValueError for BoxBlur filter with negative radius [#&#8203;6874](https://github.com/python-pillow/Pillow/issues/6874)
    \[hugovk, radarhere]

-   Support arbitrary number of loaded modules on Windows [#&#8203;6761](https://github.com/python-pillow/Pillow/issues/6761)
    \[javidcf, radarhere, nulano]

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNS41NC4wIiwidXBkYXRlZEluVmVyIjoiMzUuNTQuMCJ9-->

Co-authored-by: Renovate <renovate@git.end-play.xyz>
Reviewed-on: https://git.end-play.xyz/profitroll/TelegramPoster/pulls/29
Co-authored-by: Renovate <renovate@noreply.localhost>
Co-committed-by: Renovate <renovate@noreply.localhost>
2023-07-03 11:37:48 +03:00
15b272ae35 max_concurrent_transmissions is now 3 by default 2023-07-01 15:43:07 +02:00
28b5449f2a Improved /shutdown 2023-06-30 11:34:15 +02:00
bfec702bef Config cleanup 2023-06-30 11:34:06 +02:00
fd0c4c0545 Bump libbot to 1.7 2023-06-30 11:33:34 +02:00
11dbf3239d Removed deprecated collection 2023-06-28 10:52:00 +02:00
3d87f035e7 Added /language for owner 2023-06-28 10:48:14 +02:00
d8245934e2 Fixed wrong db record 2023-06-28 10:45:23 +02:00
420a4cb7eb Fixed locale strings and commands 2023-06-28 10:43:13 +02:00
b747dde664 Added missing requirement 2023-06-28 10:39:39 +02:00
10c60ae932 WIP: /language system 2023-06-28 10:37:18 +02:00
6f8b560acc WIP: New User system 2023-06-28 10:15:45 +02:00
93f3439a11 Fixed response type check 2023-06-28 08:59:40 +02:00
9e0a815062 Fixed markup 2023-06-28 08:57:09 +02:00
51da210817 Small fix 2023-06-28 08:56:21 +02:00
e06cb4b377 /remove command fixed 2023-06-28 08:53:15 +02:00
97b3aa1505 Starting scripts and README were updated 2023-06-28 08:22:18 +02:00
5adb004a2a API usage overhaul (#27)
* `/report` command added
* Updated to libbot 1.5
* Moved to [PhotosAPI_Client](https://git.end-play.xyz/profitroll/PhotosAPI_Client) v0.5.0 from using self-made API client
* Video support (almost stable)
* Bug fixes and improvements

Co-authored-by: profitroll <vozhd.kk@gmail.com>
Reviewed-on: #27
2023-06-28 00:57:30 +03:00
f003638128 Update 'requirements.txt' 2023-06-28 00:57:13 +03:00
e9e68cb6b3 Update dependency pymongo to v4.4.0 (#25)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [pymongo](https://github.com/mongodb/mongo-python-driver) | minor | `==4.3.3` -> `==4.4.0` |

---

### Release Notes

<details>
<summary>mongodb/mongo-python-driver</summary>

### [`v4.4.0`](https://github.com/mongodb/mongo-python-driver/releases/tag/4.4.0): PyMongo 4.4.0

[Compare Source](https://github.com/mongodb/mongo-python-driver/compare/4.3.3...4.4.0)

Release notes: https://www.mongodb.com/community/forums/t/pymongo-4-4-released/232211

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNS41NC4wIiwidXBkYXRlZEluVmVyIjoiMzUuNTQuMCJ9-->

Co-authored-by: Renovate <renovate@git.end-play.xyz>
Reviewed-on: #25
Co-authored-by: Renovate <renovate@noreply.localhost>
Co-committed-by: Renovate <renovate@noreply.localhost>
2023-06-21 22:45:23 +03:00
e5b2584d4c Bug fixes and Pyrogram bump (#23)
Co-authored-by: Renovate <renovate@git.end-play.xyz>
Co-authored-by: profitroll <vozhd.kk@gmail.com>
Reviewed-on: #23
2023-05-16 15:45:23 +03:00
29 changed files with 980 additions and 876 deletions

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/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/src/branch/master/LICENSE"><img alt="License: GPL" src="https://img.shields.io/badge/License-GPL-blue"></a>
<a href="https://git.end-play.xyz/profitroll/TelegramPoster"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a> <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/dev/README_uk.md) знаходиться) > Шукаєш інструкцію українською? А вона [ось тут](https://git.end-play.xyz/profitroll/TelegramPoster/src/branch/master/README_uk.md) знаходиться)
This bot is used for one and only task - post pictures from my personal archive. Here's its source code so you can also host a bot and have fun with it. Just don't exepect it to be brilliant. It is not. But hey, you can always fork it ;) This bot is used for one and only task - post pictures from my personal archive. Here's its source code so you can also host a bot and have fun with it. Just don't exepect it to be brilliant. It is not. But hey, you can always fork it ;)
## Dependencies ## Dependencies
* [Python 3.7+](https://www.python.org) (3.9+ recommended) * [Python 3.8+](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. You can also ignore git and simply download source code, should also work fine. After that you're ready to go. To make this bot run at first you need to have a Python interpreter, Photos API, MongoDB and optionally git (if you want to update using `git pull`). You can also ignore git and simply download source code, should also work fine. After that you're ready to go.
> In this README I assume that you're using default python in your > In this README I assume that you're using default python in your
> system and your system's PATH contains it. If your default python > system and your system's PATH contains it. If your default python
@@ -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 Mongo and Photos API: 1. Install MongoDB 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 env` 2. Create venv: `python -m venv .venv`
3. Activate it using `source venv/bin/activate` on Linux, `venv\Scripts\activate.bat` in CMD or `venv\Scripts\Activate.ps1` in PowerShell. 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 "bot" and "owner" with your favorite text editor: 5. Configure required keys with your favorite text editor:
1. Copy file `config_example.json` to `config.json` 1. Copy config file: `cp config_example.json config.json`
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 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
3. Change `"owner"`, `"bot.api_id"`, `"bot.api_hash"` and `"bot.bot_token"` keys' values. 3. Change `"bot.owner"`, `"bot.api_id"`, `"bot.api_hash"` and `"bot.bot_token"` keys' values.
If you don't know where to find bot_token and your id - here you can find some hints: [get bot token](https://www.siteguarding.com/en/how-to-get-telegram-bot-api-token), [get your id](https://www.alphr.com/telegram-find-user-id), [get api_hash and api_id](https://core.telegram.org/api/obtaining_api_id). 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"` to the one your API servers uses 1. Change `"posting.api.address"` and `"posting.api.address_external"` to the ones your API server uses
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. 2. Run your bot using `python main.py --create-user --create-album` to configure its new user and album. You can also use manual user and album creation described [in the wiki](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Configuring-API). You can also change username, password and album in`"posting.api"` to the user and album you have if you already have Photos API album and user set up. In that case you don't need to create a new one.
7. Add bot to the channel: 7. Add bot to the channel:
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. To use your bot of course you need to have a channel or group otherwise it makes no sense to have such a bot. [Here](https://stackoverflow.com/a/33497769) you can find a quick guide how to add your bot to a channel. After that simply set `"posting.channel"` to your channel's ID and `"posting.comments"` to comments group's ID.
8. Configure posting time: 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 poster.py` to start it. Make sure MongoDB and Photos API are running and use `python main.py` to start the bot.
Or you can also use `.\start.bat` on Windows and `bash ./start.sh` on Linux. Or you can also use `.\start.bat` on Windows and `bash ./start.sh` on Linux.
Additionally there are `loop.sh` and `loop.bat` available if you want your bot to start again after being stopped or after using `/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.
## Command line arguments ## CLI 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 poster.py --create-user` * `python main.py --create-user`
* `python poster.py --create-user --create-album` * `python main.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` 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"`. All localization files are located in the `locale`. Just copy locale file of your choice, name it in accordance to [IETF language tags](https://en.wikipedia.org/wiki/IETF_language_tag) (if you want your locale to be compatible with Telegram's locales) or define your own name. Save it as json and you're good to go. If you want to change default locale for messages - edit `"locale"` parameter in the `config.json`.
We recommend to only make changes to your custom locale. Or at least always have your backup of for example `en.json` as your fallback. We recommend to only make changes to your custom locale. Or at least always have your backup of for example `en.json` as your fallback.

View File

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

View File

@@ -1,29 +0,0 @@
from dataclasses import dataclass
from typing import List, Union
from pyrogram.types import (
BotCommandScopeAllChatAdministrators,
BotCommandScopeAllGroupChats,
BotCommandScopeAllPrivateChats,
BotCommandScopeChat,
BotCommandScopeChatAdministrators,
BotCommandScopeChatMember,
BotCommandScopeDefault,
BotCommand,
)
@dataclass
class CommandSet:
"""Command stored in PyroClient's 'commands' attribute"""
commands: List[BotCommand]
scope: Union[
BotCommandScopeDefault,
BotCommandScopeAllPrivateChats,
BotCommandScopeAllGroupChats,
BotCommandScopeAllChatAdministrators,
BotCommandScopeChat,
BotCommandScopeChatAdministrators,
BotCommandScopeChatMember,
] = BotCommandScopeDefault
language_code: str = ""

View File

@@ -4,5 +4,5 @@ from enum import Enum
class SubmissionType(Enum): class SubmissionType(Enum):
DOCUMENT = "document" DOCUMENT = "document"
VIDEO = "video" VIDEO = "video"
ANIMATION = "animation" # ANIMATION = "animation"
PHOTO = "photo" PHOTO = "photo"

View File

@@ -1,220 +1,130 @@
import contextlib import contextlib
import logging import logging
from datetime import datetime, timedelta from datetime import datetime
from io import BytesIO from io import BytesIO
from os import getpid, makedirs, remove, sep from os import makedirs, remove, sep
from pathlib import Path from pathlib import Path
from shutil import rmtree from shutil import rmtree
from time import time from time import time
from traceback import format_exc from traceback import format_exc
from typing import List, Tuple, Union from typing import Dict, List, Tuple, Union
import pyrogram 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 dateutil.relativedelta import relativedelta from libbot import json_write
from libbot import json_read, json_write
from libbot.i18n import BotLocale
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.client import Client from pyrogram.errors import bad_request_400
from pyrogram.errors import BadRequest, bad_request_400 from pyrogram.types import Message, User
from pyrogram.handlers.message_handler import MessageHandler
from pyrogram.raw.all import layer
from pyrogram.types import (
BotCommand,
BotCommandScopeAllChatAdministrators,
BotCommandScopeAllGroupChats,
BotCommandScopeAllPrivateChats,
BotCommandScopeChat,
BotCommandScopeChatAdministrators,
BotCommandScopeChatMember,
BotCommandScopeDefault,
Message,
)
from pytimeparse.timeparse import timeparse from pytimeparse.timeparse import timeparse
from ujson import dumps, loads from ujson import dumps, loads
from classes.commandset import CommandSet from classes.enums.submission_types import SubmissionType
from classes.exceptions import ( from classes.exceptions import (
SubmissionDuplicatesError, SubmissionDuplicatesError,
SubmissionUnavailableError, SubmissionUnavailableError,
SubmissionUnsupportedError, SubmissionUnsupportedError,
) )
from classes.pyrocommand import PyroCommand from classes.pyrouser import PyroUser
from modules.api_client import ( from modules.api_client import (
BodyPhotoUploadAlbumsAlbumPhotosPost, BodyPhotoUpload,
BodyVideoUpload,
File, File,
Photo,
Video,
client, client,
photo_upload, photo_upload,
video_upload,
) )
from modules.database import col_submitted from modules.database import col_submitted, col_users
from modules.http_client import http_session from modules.http_client import http_session
from modules.scheduler import scheduler
from modules.sender import send_content from modules.sender import send_content
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class PyroClient(Client): class PyroClient(PyroClient):
def __init__(self): def __init__(self, scheduler: AsyncIOScheduler):
with open("config.json", "r", encoding="utf-8") as f: super().__init__(locales_root=Path("locale"), scheduler=scheduler)
self.config: dict = loads(f.read())
super().__init__( self.version: float = 0.3
name="bot_client",
api_id=self.config["bot"]["api_id"],
api_hash=self.config["bot"]["api_hash"],
bot_token=self.config["bot"]["bot_token"],
plugins=dict(root="plugins", exclude=self.config["disabled_plugins"]),
sleep_threshold=120,
max_concurrent_transmissions=self.config["bot"][
"max_concurrent_transmissions"
],
)
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.commands: List[PyroCommand] = []
self.scoped_commands: bool = self.config["bot"]["scoped_commands"]
self.start_time: float = 0
self.bot_locale: BotLocale = BotLocale(Path(self.config["locations"]["locale"]))
self.default_locale: str = self.bot_locale.default
self.locales: dict = self.bot_locale.locales
self._ = self.bot_locale._
self.in_all_locales = self.bot_locale.in_all_locales
self.in_every_locale = self.bot_locale.in_every_locale
self.sender_session = ClientSession() self.sender_session = ClientSession()
self.scopes_placeholders: Dict[str, int] = {
"owner": self.owner,
"comments": self.config["posting"]["comments"],
}
async def start(self): async def start(self):
await super().start() await super().start()
self.start_time = time() if self.config["reports"]["update"]:
try:
async with ClientSession(
json_serialize=dumps,
) as http_session:
check_update = await http_session.get(
"https://git.end-play.xyz/api/v1/repos/profitroll/TelegramPoster/releases?page=1&limit=1"
)
logger.info( response = await check_update.json()
"Bot is running with Pyrogram v%s (Layer %s) and has started as @%s on PID %s.",
pyrogram.__version__,
layer,
self.me.username,
getpid(),
)
try: if len(response) == 0:
if Path(f"{self.config['locations']['cache']}/shutdown_time").exists(): raise ValueError("No bot releases on git found.")
downtime = relativedelta(
datetime.now(),
datetime.fromtimestamp(
(
await json_read(
Path(
f"{self.config['locations']['cache']}/shutdown_time"
)
)
)["timestamp"]
),
)
if downtime.days >= 1: if float(response[0]["tag_name"].replace("v", "")) > self.version:
startup_message = self._( logger.info(
"startup_downtime_days", "New version %s found (current %s)",
"message", response[0]["tag_name"].replace("v", ""),
).format(getpid(), downtime.days) self.version,
elif downtime.hours >= 1: )
startup_message = self._( await self.send_message(
"startup_downtime_hours", self.owner,
"message", self._(
).format(getpid(), downtime.hours) "update_available",
"message",
).format(
response[0]["tag_name"],
response[0]["html_url"],
response[0]["body"],
),
)
else: else:
startup_message = self._( logger.info("No updates found, bot is up to date.")
"startup_downtime_minutes", except bad_request_400.PeerIdInvalid:
"message", logger.warning(
).format(getpid(), downtime.minutes) "Could not send startup message to bot owner. Perhaps user has not started the bot yet."
)
except Exception as exp:
logger.exception("Update check failed due to %s: %s", exp, format_exc())
if self.config["mode"]["post"]:
if self.config["posting"]["use_interval"]:
self.scheduler.add_job(
send_content,
"interval",
seconds=timeparse(self.config["posting"]["interval"]),
args=[self, self.sender_session],
)
else: else:
startup_message = (self._("startup", "message").format(getpid()),) for entry in self.config["posting"]["time"]:
dt_obj = datetime.strptime(entry, "%H:%M")
await self.send_message( self.scheduler.add_job(
chat_id=self.config["reports"]["chat_id"],
text=startup_message,
)
if self.config["reports"]["update"]:
try:
async with ClientSession(
json_serialize=dumps,
) as http_session:
check_update = await http_session.get(
"https://git.end-play.xyz/api/v1/repos/profitroll/TelegramPoster/releases?page=1&limit=1"
)
response = await check_update.json()
if len(response) == 0:
raise ValueError("No bot releases on git found.")
if float(response[0]["tag_name"].replace("v", "")) > self.version:
logger.info(
"New version %s found (current %s)",
response[0]["tag_name"].replace("v", ""),
self.version,
)
await self.send_message(
self.owner,
self._(
"update_available",
"message",
).format(
response[0]["tag_name"],
response[0]["html_url"],
response[0]["body"],
),
)
else:
logger.info("No updates found, bot is up to date.")
except bad_request_400.PeerIdInvalid:
logger.warning(
"Could not send startup message to bot owner. Perhaps user has not started the bot yet."
)
except Exception as exp:
logger.exception(
"Update check failed due to %s: %s", exp, format_exc()
)
scheduler.add_job(
self.register_commands,
trigger="date",
run_date=datetime.now() + timedelta(seconds=5),
kwargs={"command_sets": await self.collect_commands()},
)
if self.config["mode"]["post"]:
if self.config["posting"]["use_interval"]:
scheduler.add_job(
send_content, send_content,
"interval", "cron",
seconds=timeparse(self.config["posting"]["interval"]), hour=dt_obj.hour,
minute=dt_obj.minute,
args=[self, self.sender_session], args=[self, self.sender_session],
) )
else:
for entry in self.config["posting"]["time"]:
dt_obj = datetime.strptime(entry, "%H:%M")
scheduler.add_job(
send_content,
"cron",
hour=dt_obj.hour,
minute=dt_obj.minute,
args=[self, self.sender_session],
)
scheduler.start()
except BadRequest:
logger.warning("Unable to send message to report chat.")
async def stop(self): async def stop(self):
makedirs(self.config["locations"]["cache"], exist_ok=True) makedirs(self.config["locations"]["cache"], exist_ok=True)
@@ -223,182 +133,12 @@ class PyroClient(Client):
Path(f"{self.config['locations']['cache']}/shutdown_time"), Path(f"{self.config['locations']['cache']}/shutdown_time"),
) )
try:
await self.send_message(
chat_id=self.config["reports"]["chat_id"],
text=f"Bot stopped with PID `{getpid()}`",
)
except BadRequest:
logger.warning("Unable to send message to report chat.")
await http_session.close() await http_session.close()
await self.sender_session.close() await self.sender_session.close()
await super().stop() await super().stop()
logger.warning("Bot stopped with PID %s.", getpid())
async def collect_commands(self) -> Union[List[CommandSet], None]: async def submit_media(
"""Gather list of the bot's commands
### Returns:
* `List[CommandSet]`: List of the commands' sets
"""
command_sets = None
# If config get bot.scoped_commands is true - more complicated
# scopes system will be used instead of simple global commands
if self.scoped_commands:
scopes = {}
command_sets = []
# Iterate through all commands in config
for command, contents in self.config["commands"].items():
# Iterate through all scopes of a command
for scope in contents["scopes"]:
if dumps(scope) not in scopes:
scopes[dumps(scope)] = {"_": []}
# Add command to the scope's flattened key in scopes dict
scopes[dumps(scope)]["_"].append(
BotCommand(command, _(command, "commands"))
)
for locale, string in (
self.in_every_locale(command, "commands")
).items():
if locale not in scopes[dumps(scope)]:
scopes[dumps(scope)][locale] = []
scopes[dumps(scope)][locale].append(BotCommand(command, string))
# Iterate through all scopes and its commands
for scope, locales in scopes.items():
# Make flat key a dict again
scope_dict = loads(scope)
# Replace "owner" in the bot scope with owner's id
if "chat_id" in scope_dict and scope_dict["chat_id"] == "owner":
scope_dict["chat_id"] = self.owner
# Create object with the same name and args from the dict
try:
scope_obj = globals()[scope_dict["name"]](
**{
key: value
for key, value in scope_dict.items()
if key != "name"
}
)
except NameError:
logger.error(
"Could not register commands of the scope '%s' due to an invalid scope class provided!",
scope_dict["name"],
)
continue
except TypeError:
logger.error(
"Could not register commands of the scope '%s' due to an invalid class arguments provided!",
scope_dict["name"],
)
continue
# Add set of commands to the list of the command sets
for locale, commands in locales.items():
if locale == "_":
command_sets.append(
CommandSet(commands, scope=scope_obj, language_code="")
)
continue
command_sets.append(
CommandSet(commands, scope=scope_obj, language_code=locale)
)
logger.info("Registering the following command sets: %s", command_sets)
else:
# This part here looks into the handlers and looks for commands
# in it, if there are any. Then adds them to self.commands
for handler in self.dispatcher.groups[0]:
if isinstance(handler, MessageHandler):
for entry in [handler.filters.base, handler.filters.other]:
if hasattr(entry, "commands"):
for command in entry.commands:
logger.info("I see a command %s in my filters", command)
self.add_command(command)
return command_sets
def add_command(
self,
command: str,
):
"""Add command to the bot's internal commands list
### Args:
* command (`str`)
"""
self.commands.append(
PyroCommand(
command,
_(command, "commands"),
)
)
logger.info(
"Added command '%s' to the bot's internal commands list",
command,
)
async def register_commands(
self, command_sets: Union[List[CommandSet], None] = None
):
"""Register commands stored in bot's 'commands' attribute"""
if command_sets is None:
commands = [
BotCommand(command=command.command, description=command.description)
for command in self.commands
]
logger.info(
"Registering commands %s with a default scope 'BotCommandScopeDefault'"
)
await self.set_bot_commands(commands)
return
for command_set in command_sets:
logger.info(
"Registering command set with commands %s and scope '%s' (%s)",
command_set.commands,
command_set.scope,
command_set.language_code,
)
await self.set_bot_commands(
command_set.commands,
command_set.scope,
language_code=command_set.language_code,
)
async def remove_commands(self, command_sets: Union[List[CommandSet], None] = None):
"""Remove commands stored in bot's 'commands' attribute"""
if command_sets is None:
logger.info(
"Removing commands with a default scope 'BotCommandScopeDefault'"
)
await self.delete_bot_commands(BotCommandScopeDefault())
return
for command_set in command_sets:
logger.info(
"Removing command set with scope '%s' (%s)",
command_set.scope,
command_set.language_code,
)
await self.delete_bot_commands(
command_set.scope,
language_code=command_set.language_code,
)
async def submit_photo(
self, id: str self, id: str
) -> Tuple[Union[Message, None], Union[str, None]]: ) -> Tuple[Union[Message, None], Union[str, None]]:
db_entry = col_submitted.find_one({"_id": ObjectId(id)}) db_entry = col_submitted.find_one({"_id": ObjectId(id)})
@@ -416,7 +156,7 @@ class PyroClient(Client):
submission, file_name=self.config["locations"]["tmp"] + sep submission, file_name=self.config["locations"]["tmp"] + sep
) )
except Exception as exp: except Exception as exp:
raise SubmissionUnavailableError() 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']}",
@@ -431,24 +171,47 @@ class PyroClient(Client):
db_entry["user"], db_entry["telegram"]["msg_id"] db_entry["user"], db_entry["telegram"]["msg_id"]
) )
with open(str(filepath), "rb") as fh: async with aiofiles.open(str(filepath), "rb") as fh:
photo_bytes = BytesIO(fh.read()) media_bytes = BytesIO(await fh.read())
try: try:
response = await photo_upload( if db_entry["type"] == SubmissionType.PHOTO.value:
self.config["posting"]["api"]["album"], response = await photo_upload(
client=client, self.config["posting"]["api"]["album"],
multipart_data=BodyPhotoUploadAlbumsAlbumPhotosPost( client=client,
File(photo_bytes, filepath.name, "image/jpeg") multipart_data=BodyPhotoUpload(
), File(media_bytes, filepath.name, "image/jpeg")
ignore_duplicates=self.config["submission"]["allow_duplicates"], ),
compress=False, ignore_duplicates=self.config["submission"]["allow_duplicates"],
caption="queue", compress=False,
) caption="queue",
except UnexpectedStatus: )
raise SubmissionUnsupportedError(str(filepath)) elif db_entry["type"] == SubmissionType.VIDEO.value:
response = await video_upload(
self.config["posting"]["api"]["album"],
client=client,
multipart_data=BodyVideoUpload(
File(media_bytes, filepath.name, "video/*")
),
caption="queue",
)
# elif db_entry["type"] == SubmissionType.ANIMATION.value:
# response = await video_upload(
# self.config["posting"]["api"]["album"],
# client=client,
# multipart_data=BodyVideoUpload(
# File(media_bytes, filepath.name, "video/*")
# ),
# caption="queue",
# )
except UnexpectedStatus as exp:
raise SubmissionUnsupportedError(str(filepath)) from exp
response_dict = loads(response.content.decode("utf-8")) response_dict = (
{}
if not hasattr(response, "content")
else loads(response.content.decode("utf-8"))
)
if "duplicates" in response_dict and len(response_dict["duplicates"]) > 0: if "duplicates" in response_dict and len(response_dict["duplicates"]) > 0:
duplicates = [] duplicates = []
@@ -480,10 +243,38 @@ class PyroClient(Client):
except (FileNotFoundError, NotADirectoryError): except (FileNotFoundError, NotADirectoryError):
logger.error("Could not delete '%s' on submission accepted", filepath) logger.error("Could not delete '%s' on submission accepted", filepath)
return submission, response.parsed.id return (
submission,
response.parsed.id if hasattr(response, "parsed") else response.id,
)
async def ban_user(self, id: int) -> None: async def find_user(self, user: Union[int, User]) -> PyroUser:
pass """Find User by it's ID or User object
async def unban_user(self, id: int) -> None: ### Args:
pass * user (`Union[int, User]`): ID or User object to extract ID from
### Returns:
* `PyroUser`: PyroUser object
"""
if (
col_users.find_one(
{"id": user.id if isinstance(user, User) else user}
) # type: ignore
is None
):
col_users.insert_one(
{
"id": user.id if isinstance(user, User) else user,
"locale": user.language_code if isinstance(user, User) else None,
"banned": False,
"cooldown": datetime(1970, 1, 1, 0, 0),
"subscription": {"expires": datetime(1970, 1, 1, 0, 0)},
}
) # type: ignore
db_record = col_users.find_one(
{"id": user.id if isinstance(user, User) else user}
) # type: ignore
return PyroUser(**db_record)

View File

@@ -1,9 +0,0 @@
from dataclasses import dataclass
@dataclass
class PyroCommand:
"""Command stored in PyroClient's 'commands' attribute"""
command: str
description: str

55
classes/pyrouser.py Normal file
View File

@@ -0,0 +1,55 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Union
from bson import ObjectId
from libbot import config_get
from libbot.pyrogram.classes import PyroClient
from modules.database import col_users
@dataclass
class PyroUser:
"""Dataclass of DB entry of a user"""
_id: ObjectId
id: int
locale: Union[str, None]
banned: bool
cooldown: datetime
subscription: dict
async def update_locale(self, locale: str):
col_users.update_one({"_id": self._id}, {"$set": {"locale": locale}})
async def update_cooldown(self, time: datetime = datetime.now()):
col_users.update_one({"_id": self._id}, {"$set": {"cooldown": time}})
async def block(self) -> None:
"""Ban user from using command and submitting content."""
col_users.update_one({"_id": self._id}, {"$set": {"banned": True}})
async def unblock(self) -> None:
"""Allow user to use command and submit posts again."""
col_users.update_one({"_id": self._id}, {"$set": {"banned": False}})
async def is_limited(self, app: Union[PyroClient, None] = None) -> bool:
"""Check if user is on a cooldown after submitting something.
### Returns:
`bool`: Must be `True` if on the cooldown and `False` if not
"""
admins = (
app.admins
if app is not None
else (
await config_get("admins", "bot") + [await config_get("owner", "bot")]
)
)
return (datetime.now() - self.cooldown).total_seconds() < (
app.config["submission"]["timeout"]
if app is not None
else await config_get("timeout", "submission")
)

View File

@@ -1,58 +0,0 @@
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,14 +1,12 @@
{ {
"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": 5, "max_concurrent_transmissions": 1,
"scoped_commands": true "scoped_commands": true
}, },
"database": { "database": {
@@ -19,7 +17,7 @@
"name": "tgposter" "name": "tgposter"
}, },
"reports": { "reports": {
"chat_id": 0, "chat_id": "owner",
"sent": false, "sent": false,
"error": true, "error": true,
"update": true, "update": true,
@@ -37,30 +35,29 @@
"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": {
"channel": 0, "channel": 0,
"comments": 0,
"silent": false, "silent": false,
"move_sent": false, "move_sent": false,
"use_interval": false, "use_interval": false,
"interval": "1h30m", "interval": "1h30m",
"page_size": 300,
"submitted_caption": { "submitted_caption": {
"enabled": true, "enabled": true,
"ignore_admins": true, "ignore_admins": true,
"text": "#submitted" "text": "#submitted"
}, },
"types": {
"photo": true,
"video": false
},
"extensions": { "extensions": {
"photo": [ "photo": [
"jpg", "jpg",
"png", "png",
"gif",
"jpeg" "jpeg"
], ],
"video": [ "video": [
@@ -108,7 +105,6 @@
}, },
"mime_types": [ "mime_types": [
"image/png", "image/png",
"image/gif",
"image/jpeg", "image/jpeg",
"video/mp4", "video/mp4",
"video/quicktime" "video/quicktime"
@@ -137,6 +133,25 @@
} }
] ]
}, },
"language": {
"scopes": [
{
"name": "BotCommandScopeDefault"
},
{
"name": "BotCommandScopeChat",
"chat_id": "owner"
}
]
},
"report": {
"scopes": [
{
"name": "BotCommandScopeChat",
"chat_id": "comments"
}
]
},
"forwards": { "forwards": {
"scopes": [ "scopes": [
{ {

View File

@@ -1,7 +1,18 @@
{ {
"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",
"forwards": "Check post forwards", "forwards": "Check post forwards",
"import": "Submit .zip archive with photos", "import": "Submit .zip archive with photos",
"export": "Get .zip archive with all photos", "export": "Get .zip archive with all photos",
@@ -53,13 +64,18 @@
"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_received": "This message has been reported by **{0}** (@{1}, `{2}`)"
}, },
"button": { "button": {
"sub_yes": "✅ Accept", "sub_yes": "✅ Accept",
@@ -70,7 +86,9 @@
"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",
@@ -81,50 +99,7 @@
"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,7 +1,17 @@
{ {
"metadata": {
"flag": "🇺🇦",
"name": "Українська",
"codes": [
"uk",
"uk-UA"
]
},
"commands": { "commands": {
"start": "Почати користуватись ботом", "start": "Почати користуватись ботом",
"rules": "Правила пропонування фото", "rules": "Правила пропонування фото",
"language": "Змінити мову бота",
"report": "Поскаржитись на цей пост",
"forwards": "Переглянути репости", "forwards": "Переглянути репости",
"import": "Надати боту .zip архів з фотографіями", "import": "Надати боту .zip архів з фотографіями",
"export": "Отримати .zip архів з усіма фотографіями", "export": "Отримати .zip архів з усіма фотографіями",
@@ -53,13 +63,18 @@
"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_received": "На це повідомлення було отримано скаргу від **{0}** (@{1}, `{2}`)"
}, },
"button": { "button": {
"sub_yes": "✅ Прийняти", "sub_yes": "✅ Прийняти",
@@ -70,7 +85,9 @@
"post_view": "Переглянути на каналі", "post_view": "Переглянути на каналі",
"accepted": "✅ Прийнято", "accepted": "✅ Прийнято",
"declined": "❌ Відхилено", "declined": "❌ Відхилено",
"shutdown": "Підтвердити вимкнення" "shutdown": "Підтвердити вимкнення",
"photo": "Фото",
"video": "Відео"
}, },
"callback": { "callback": {
"sub_yes": "✅ Подання схвалено", "sub_yes": "✅ Подання схвалено",
@@ -81,50 +98,7 @@
"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 poster.py python main.py
echo To completely stop TelegramPoster now, please press Ctrl+C during the countdown! echo To completely stop TelegramPoster now, please press Ctrl+C during the countdown!
echo Restarting in 5 seconds... echo Restarting in 5 seconds...
Timeout /t 5 Timeout /t 5

View File

@@ -5,7 +5,7 @@
while true while true
do do
python poster.py python main.py
echo "To completely stop TelegramPoster now, please press Ctrl+C during the countdown!" echo "To completely stop TelegramPoster now, please press Ctrl+C during the countdown!"
echo "Restarting in:" echo "Restarting in:"
for i in 5 4 3 2 1 for i in 5 4 3 2 1

View File

@@ -4,6 +4,10 @@ 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
@@ -22,7 +26,7 @@ with contextlib.suppress(ImportError):
def main(): def main():
client = PyroClient() client = PyroClient(scheduler=scheduler)
Conversation(client) Conversation(client)
try: try:
@@ -30,7 +34,8 @@ def main():
except KeyboardInterrupt: except KeyboardInterrupt:
logger.warning("Forcefully shutting down with PID %s...", getpid()) logger.warning("Forcefully shutting down with PID %s...", getpid())
finally: finally:
scheduler.shutdown() if client.scheduler is not None:
client.scheduler.shutdown()
exit() exit()

View File

@@ -27,22 +27,45 @@ from photosapi_client.api.default.photo_get_photos_id_get import asyncio as phot
from photosapi_client.api.default.photo_patch_photos_id_patch import ( from photosapi_client.api.default.photo_patch_photos_id_patch import (
asyncio as photo_patch, asyncio as photo_patch,
) )
from photosapi_client.api.default.photo_random_albums_album_photos_random_get import (
asyncio as photo_random,
)
from photosapi_client.api.default.photo_upload_albums_album_photos_post import ( from photosapi_client.api.default.photo_upload_albums_album_photos_post import (
asyncio_detailed as photo_upload, asyncio_detailed as photo_upload,
) )
from photosapi_client.api.default.user_create_users_post import asyncio as user_create from photosapi_client.api.default.user_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,
) )
from photosapi_client.api.default.video_get_videos_id_get import asyncio as video_get
from photosapi_client.api.default.video_patch_videos_id_patch import (
asyncio as video_patch,
)
from photosapi_client.api.default.video_random_albums_album_videos_random_get import (
asyncio as video_random,
)
from photosapi_client.api.default.video_upload_albums_album_videos_post import (
asyncio as video_upload,
)
from photosapi_client.models.body_login_for_access_token_token_post import ( from photosapi_client.models.body_login_for_access_token_token_post import (
BodyLoginForAccessTokenTokenPost, BodyLoginForAccessTokenTokenPost,
) )
from photosapi_client.models.body_photo_upload_albums_album_photos_post import ( from photosapi_client.models.body_photo_upload_albums_album_photos_post import (
BodyPhotoUploadAlbumsAlbumPhotosPost, BodyPhotoUploadAlbumsAlbumPhotosPost as BodyPhotoUpload,
)
from photosapi_client.models.body_video_upload_albums_album_videos_post import (
BodyVideoUploadAlbumsAlbumVideosPost as BodyVideoUpload,
) )
from photosapi_client.models.http_validation_error import HTTPValidationError from photosapi_client.models.http_validation_error import HTTPValidationError
from photosapi_client.models.photo import Photo
from photosapi_client.models.photo_search import PhotoSearch
from photosapi_client.models.token import Token from photosapi_client.models.token import Token
from photosapi_client.models.video import Video
from photosapi_client.models.video_search import VideoSearch
from photosapi_client.types import File from photosapi_client.types import File
from modules.http_client import http_session from modules.http_client import http_session
@@ -77,15 +100,10 @@ async def authorize(custom_session: Union[ClientSession, None] = None) -> str:
) )
if not response.ok: if not response.ok:
logger.warning( logger.warning(
i18n._( "Incorrect API credentials! Could not login into '%s' using login '%s': HTTP %s",
"api_creds_invalid", await config_get("address", "posting", "api"),
"console", await config_get("username", "posting", "api"),
locale=(await config_get("locale_log")).format( response.status,
await config_get("address", "posting", "api"),
await config_get("username", "posting", "api"),
response.status,
),
)
) )
raise ValueError raise ValueError
async with aiofiles.open( async with aiofiles.open(
@@ -103,6 +121,7 @@ unauthorized_client = Client(
timeout=5.0, timeout=5.0,
verify_ssl=True, verify_ssl=True,
raise_on_unexpected_status=True, raise_on_unexpected_status=True,
follow_redirects=False,
) )
login_token = login( login_token = login(
@@ -127,6 +146,7 @@ client = AuthenticatedClient(
verify_ssl=True, verify_ssl=True,
raise_on_unexpected_status=True, raise_on_unexpected_status=True,
token=login_token.access_token, token=login_token.access_token,
follow_redirects=False,
) )
if __name__ == "__main__": if __name__ == "__main__":

118
modules/cli.py Normal file
View File

@@ -0,0 +1,118 @@
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 exp:
print(f"Could not create a user due to {exp}", flush=True)
print_exc()
exit()
if not args.create_album:
print("You're done!", flush=True)
exit()
return None
async def cli_create_album() -> None:
print(
"To use Photos API your user needs to have an album to store its data.\nThis wizard will help you to create a new album with its name and title.",
flush=True,
)
name = input("Choose a name for your album: ").strip()
title = input(f"Choose a title for album '{name}': ").strip()
try:
login_token = await login(
client=unauthorized_client,
form_data=BodyLoginForAccessTokenTokenPost(
grant_type="password",
scope="me albums.list albums.read albums.write photos.list photos.read photos.write videos.list videos.read videos.write",
username=await config_get("username", "posting", "api"),
password=await config_get("password", "posting", "api"),
),
)
client = AuthenticatedClient(
base_url=await config_get("address", "posting", "api"),
timeout=5.0,
verify_ssl=True,
raise_on_unexpected_status=True,
token=login_token.access_token,
follow_redirects=False,
)
result_2 = await album_create(client=client, name=name, title=title)
# asyncio.run(create_album(name, title))
await config_set("album", name, "posting", "api")
except Exception as exp:
print(f"Could not create an album due to {exp}", flush=True)
print_exc()
exit()
print("You're done!", flush=True)
exit()
if args.create_user or args.create_album:
loop = asyncio.get_event_loop()
tasks = []
if args.create_user:
loop.run_until_complete(asyncio.wait([loop.create_task(cli_create_user())]))
if args.create_album:
loop.run_until_complete(asyncio.wait([loop.create_task(cli_create_album())]))
loop.close()

View File

@@ -25,11 +25,10 @@ db = db_client.get_database(name=db_config["name"])
collections = db.list_collection_names() collections = db.list_collection_names()
for collection in ["sent", "users", "banned", "submitted"]: for collection in ["sent", "users", "submitted"]:
if not collection in collections: if collection not in collections:
db.create_collection(collection) 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

@@ -1,24 +1,37 @@
import logging import logging
from datetime import datetime from datetime import datetime
from os import makedirs, path from os import makedirs, path
from random import choice from random import choice, sample
from shutil import rmtree from shutil import rmtree
from traceback import format_exc from traceback import format_exc, print_exc
from typing import Union
from uuid import uuid4 from uuid import uuid4
import aiofiles import aiofiles
from aiohttp import ClientSession from aiohttp import ClientSession
from libbot.pyrogram.classes import PyroClient
from photosapi_client.errors import UnexpectedStatus from photosapi_client.errors import UnexpectedStatus
from PIL import Image from PIL import Image
from pyrogram.client import Client
from modules.api_client import authorize, client, photo_find, photo_patch from modules.api_client import (
File,
PhotoSearch,
VideoSearch,
authorize,
client,
photo_get,
photo_patch,
photo_random,
video_get,
video_patch,
video_random,
)
from modules.database import col_sent, col_submitted from modules.database import col_sent, col_submitted
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def send_content(app: Client, http_session: ClientSession) -> None: async def send_content(app: PyroClient, http_session: ClientSession) -> None:
try: try:
try: try:
token = await authorize(http_session) token = await authorize(http_session)
@@ -30,24 +43,84 @@ async def send_content(app: Client, http_session: ClientSession) -> None:
return return
try: try:
pic = choice( funcs = []
(
await photo_find( if app.config["posting"]["types"]["photo"]:
funcs.append((photo_random, photo_get, app.send_photo, photo_patch))
if app.config["posting"]["types"]["video"]:
funcs.append((video_random, video_get, app.send_video, video_patch))
if not funcs:
raise KeyError(
"No media source provided: all seem to be disabled in config"
)
if len(funcs) > 1:
found = False
for func_iter in sample(funcs, len(funcs)):
func = func_iter
random_results = (
await func_iter[0](
album=app.config["posting"]["api"]["album"],
caption="queue",
client=client,
limit=1,
)
).results
if not random_results:
continue
media: Union[PhotoSearch, VideoSearch] = random_results[0]
try:
response: File = await func_iter[1](id=media.id, client=client)
except Exception as exp:
print_exc()
logger.error("Media is invalid: %s", exp)
if app.config["reports"]["error"]:
await app.send_message(
app.owner, f"Media is invalid: {exp}"
)
return
found = True
break
if not found:
raise KeyError("No media found")
else:
func = funcs[0]
media: Union[PhotoSearch, VideoSearch] = (
await func[0](
album=app.config["posting"]["api"]["album"], album=app.config["posting"]["api"]["album"],
caption="queue", caption="queue",
page_size=app.config["posting"]["page_size"],
client=client, client=client,
limit=1,
) )
).results ).results[0]
) try:
response: File = await func[1](id=media.id, client=client)
except Exception as exp:
print_exc()
logger.error("Media is invalid: %s", exp)
if app.config["reports"]["error"]:
await app.send_message(app.owner, f"Media is invalid: {exp}")
return
except (KeyError, AttributeError, TypeError, IndexError): except (KeyError, AttributeError, TypeError, IndexError):
logger.info(app._("post_empty", "console")) logger.info(
"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,
app._("api_queue_empty", "message"), app._("api_queue_empty", "message"),
) )
return return
except (ValueError, UnexpectedStatus): except (ValueError, UnexpectedStatus):
if app.config["reports"]["error"]: if app.config["reports"]["error"]:
await app.send_message( await app.send_message(
@@ -56,41 +129,27 @@ async def send_content(app: Client, http_session: ClientSession) -> None:
) )
return return
response = await http_session.get(
f"{app.config['posting']['api']['address']}/photos/{pic.id}",
headers={"Authorization": f"Bearer {token}"},
)
if response.status != 200:
logger.warning(
app._("post_invalid_pic", "console").format(
response.status, str(await response.json())
)
)
if app.config["reports"]["error"]:
await app.send_message(
app.owner,
app._("post_invalid_pic", "message").format(
response.status, await response.json()
),
)
tmp_dir = str(uuid4()) tmp_dir = str(uuid4())
makedirs(path.join(app.config["locations"]["tmp"], tmp_dir), exist_ok=True) makedirs(path.join(app.config["locations"]["tmp"], tmp_dir), exist_ok=True)
tmp_path = path.join(tmp_dir, pic.filename) tmp_path = path.join(tmp_dir, media.filename)
async with aiofiles.open( async with aiofiles.open(
path.join(app.config["locations"]["tmp"], tmp_path), "wb" path.join(app.config["locations"]["tmp"], tmp_path), "wb"
) as out_file: ) as out_file:
await out_file.write(await response.read()) await out_file.write(response.payload.read())
logger.info( logger.info(
f"Candidate {pic.filename} ({pic.id}) is {path.getsize(path.join(app.config['locations']['tmp'], tmp_path))} bytes big", "Candidate %s (%s) is %s bytes big",
media.filename,
media.id,
path.getsize(path.join(app.config["locations"]["tmp"], tmp_path)),
) )
if path.getsize(path.join(app.config["locations"]["tmp"], tmp_path)) > 5242880: if (
path.getsize(path.join(app.config["locations"]["tmp"], tmp_path)) > 5242880
) and func[0] is photo_random:
image = Image.open(path.join(app.config["locations"]["tmp"], tmp_path)) 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.ANTIALIAS) image = image.resize((int(width / 2), int(height / 2)), Image.ANTIALIAS)
@@ -110,7 +169,9 @@ async def send_content(app: Client, http_session: ClientSession) -> None:
) )
image.close() image.close()
if path.getsize(path.join(app.config["locations"]["tmp"], tmp_path)) > 5242880: if (
path.getsize(path.join(app.config["locations"]["tmp"], tmp_path)) > 5242880
) and func[0] is photo_random:
rmtree( rmtree(
path.join(app.config["locations"]["tmp"], tmp_dir), ignore_errors=True path.join(app.config["locations"]["tmp"], tmp_dir), ignore_errors=True
) )
@@ -118,7 +179,7 @@ async def send_content(app: Client, http_session: ClientSession) -> None:
del response del response
submitted = col_submitted.find_one({"temp.file": pic.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()
@@ -150,14 +211,16 @@ async def send_content(app: Client, http_session: ClientSession) -> None:
caption = caption caption = caption
try: try:
sent = await app.send_photo( sent = await func[2](
app.config["posting"]["channel"], app.config["posting"]["channel"],
path.join(app.config["locations"]["tmp"], tmp_path), path.join(app.config["locations"]["tmp"], tmp_path),
caption=caption, caption=caption,
disable_notification=app.config["posting"]["silent"], disable_notification=app.config["posting"]["silent"],
) )
except Exception as exp: except Exception as exp:
logger.error(f"Could not send image {pic.filename} ({pic.id}) due to {exp}") logger.error(
"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,
@@ -169,8 +232,8 @@ async def send_content(app: Client, http_session: ClientSession) -> None:
col_sent.insert_one( col_sent.insert_one(
{ {
"date": datetime.now(), "date": datetime.now(),
"image": pic.id, "image": media.id,
"filename": pic.filename, "filename": media.filename,
"channel": app.config["posting"]["channel"], "channel": app.config["posting"]["channel"],
"caption": None "caption": None
if (submitted is None or submitted["caption"] is None) if (submitted is None or submitted["caption"] is None)
@@ -178,21 +241,22 @@ async def send_content(app: Client, http_session: ClientSession) -> None:
} }
) )
await photo_patch(id=pic.id, client=client, caption="sent") await func[3](id=media.id, client=client, caption="sent")
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(
app._("post_sent", "console").format( "Sent %s to %s with caption %s and silently %s",
pic.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 exp: except Exception as exp:
logger.error(app._("post_exception", "console").format(str(exp), format_exc())) logger.error(
"Could not send content due to %s. Traceback: %s", 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,

View File

@@ -7,6 +7,6 @@ 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, clb: CallbackQuery): async def callback_query_nothing(app: PyroClient, clb: CallbackQuery):
await clb.answer( user = await app.find_user(clb.from_user)
text=app._("nothing", "callback", locale=clb.from_user.language_code)
) await clb.answer(text=app._("nothing", "callback", locale=user.locale))

View File

@@ -15,7 +15,6 @@ 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__)
@@ -23,22 +22,22 @@ 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, clb: CallbackQuery): async def callback_query_yes(app: PyroClient, clb: CallbackQuery):
user = await app.find_user(clb.from_user)
fullclb = str(clb.data).split("_") fullclb = str(clb.data).split("_")
user_locale = clb.from_user.language_code
db_entry = col_submitted.find_one({"_id": ObjectId(fullclb[2])}) db_entry = col_submitted.find_one({"_id": ObjectId(fullclb[2])})
try: try:
submission = await app.submit_photo(fullclb[2]) submission = await app.submit_media(fullclb[2])
except SubmissionUnavailableError: except SubmissionUnavailableError:
await clb.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 clb.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,
@@ -46,37 +45,43 @@ async def callback_query_yes(app: PyroClient, clb: CallbackQuery):
return return
except SubmissionDuplicatesError as exp: except SubmissionDuplicatesError as exp:
await clb.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 clb.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(exp.duplicates) "\n".join(exp.duplicates)
), ),
quote=True, quote=True,
) )
logger.info( logger.info(
app._( "Submission with ID '%s' could not be accepted because of the duplicates: %s",
"submission_duplicate", fullclb[2],
"console", str(exp.duplicates),
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._("sub_yes", "message", locale=submission[0].from_user.language_code), app._(
"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(db_entry["user"], app._("sub_yes", "message")) await app.send_message(
db_entry["user"],
app._(
"sub_yes",
"message",
locale=(await app.find_user(db_entry["user"])).locale,
),
)
await clb.answer( await clb.answer(
text=app._("sub_yes", "callback", locale=user_locale).format(fullclb[2]), text=app._("sub_yes", "callback", locale=user.locale).format(fullclb[2]),
show_alert=True, show_alert=True,
) )
@@ -84,7 +89,7 @@ async def callback_query_yes(app: PyroClient, clb: 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",
) )
], ],
@@ -94,7 +99,7 @@ async def callback_query_yes(app: PyroClient, clb: CallbackQuery):
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",
) )
] ]
@@ -103,7 +108,7 @@ async def callback_query_yes(app: PyroClient, clb: CallbackQuery):
if await config_get("send_uploaded_id", "submission"): if await config_get("send_uploaded_id", "submission"):
await clb.message.edit_caption( await clb.message.edit_caption(
clb.message.caption + f"\n\nID: `{submission[1]}`" f"{clb.message.caption}\n\nID: `{submission[1]}`"
) )
await clb.message.edit_reply_markup( await clb.message.edit_reply_markup(
@@ -111,18 +116,16 @@ async def callback_query_yes(app: PyroClient, clb: CallbackQuery):
) )
logger.info( logger.info(
app._( "Submission with ID '%s' accepted and uploaded with ID '%s'",
"submission_accepted", fullclb[2],
"console", submission[1],
locale=app.config["locale_log"],
).format(fullclb[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, clb: CallbackQuery): async def callback_query_no(app: PyroClient, clb: CallbackQuery):
user = await app.find_user(clb.from_user)
fullclb = str(clb.data).split("_") fullclb = str(clb.data).split("_")
user_locale = clb.from_user.language_code
db_entry = col_submitted.find_one_and_delete({"_id": ObjectId(fullclb[2])}) db_entry = col_submitted.find_one_and_delete({"_id": ObjectId(fullclb[2])})
@@ -145,17 +148,21 @@ async def callback_query_no(app: PyroClient, clb: CallbackQuery):
) )
except Exception as exp: except Exception as exp:
await clb.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._("sub_no", "message", locale=submission.from_user.language_code), app._(
"sub_no",
"message",
locale=(await app.find_user(submission.from_user)).locale,
),
quote=True, quote=True,
) )
await clb.answer( await clb.answer(
text=app._("sub_no", "callback", locale=user_locale).format(fullclb[2]), text=app._("sub_no", "callback", locale=user.locale).format(fullclb[2]),
show_alert=True, show_alert=True,
) )
@@ -163,7 +170,7 @@ async def callback_query_no(app: PyroClient, clb: 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",
) )
], ],
@@ -173,7 +180,7 @@ async def callback_query_no(app: PyroClient, clb: CallbackQuery):
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",
) )
] ]
@@ -183,27 +190,28 @@ async def callback_query_no(app: PyroClient, clb: CallbackQuery):
reply_markup=InlineKeyboardMarkup(edited_markup) reply_markup=InlineKeyboardMarkup(edited_markup)
) )
logger.info( logger.info(
app._( "Submission with ID '%s' rejected",
"submission_rejected", fullclb[2],
"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, clb: CallbackQuery): async def callback_query_block(app: PyroClient, clb: CallbackQuery):
user = await app.find_user(clb.from_user)
fullclb = str(clb.data).split("_") fullclb = str(clb.data).split("_")
user_locale = clb.from_user.language_code
await app.send_message( await app.send_message(
int(fullclb[2]), int(fullclb[2]),
app._("sub_blocked", "message"), app._(
"sub_blocked",
"message",
locale=(await app.find_user(int(fullclb[2]))).locale,
),
) )
PosterUser(int(fullclb[2])).block() await user.block()
await clb.answer( await clb.answer(
text=app._("sub_block", "callback", locale=user_locale).format(fullclb[2]), text=app._("sub_block", "callback", locale=user.locale).format(fullclb[2]),
show_alert=True, show_alert=True,
) )
@@ -211,7 +219,7 @@ async def callback_query_block(app: PyroClient, clb: CallbackQuery):
clb.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_{fullclb[2]}", callback_data=f"sub_unblock_{fullclb[2]}",
) )
], ],
@@ -219,26 +227,27 @@ async def callback_query_block(app: PyroClient, clb: CallbackQuery):
await clb.message.edit_reply_markup( await clb.message.edit_reply_markup(
reply_markup=InlineKeyboardMarkup(edited_markup) reply_markup=InlineKeyboardMarkup(edited_markup)
) )
logger.info( logger.info("User %s has been blocked", fullclb[2])
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, clb: CallbackQuery): async def callback_query_unblock(app: PyroClient, clb: CallbackQuery):
user = await app.find_user(clb.from_user)
fullclb = str(clb.data).split("_") fullclb = str(clb.data).split("_")
user_locale = clb.from_user.language_code
await app.send_message(int(fullclb[2]), app._("sub_unblocked", "message")) await app.send_message(
int(fullclb[2]),
app._(
"sub_unblocked",
"message",
locale=(await app.find_user(int(fullclb[2]))).locale,
),
)
PosterUser(int(fullclb[2])).unblock() await user.unblock()
await clb.answer( await clb.answer(
text=app._("sub_unblock", "callback", locale=user_locale).format(fullclb[2]), text=app._("sub_unblock", "callback", locale=user.locale).format(fullclb[2]),
show_alert=True, show_alert=True,
) )
@@ -246,7 +255,7 @@ async def callback_query_unblock(app: PyroClient, clb: CallbackQuery):
clb.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_{fullclb[2]}", callback_data=f"sub_block_{fullclb[2]}",
) )
], ],
@@ -254,10 +263,4 @@ async def callback_query_unblock(app: PyroClient, clb: CallbackQuery):
await clb.message.edit_reply_markup( await clb.message.edit_reply_markup(
reply_markup=InlineKeyboardMarkup(edited_markup) reply_markup=InlineKeyboardMarkup(edited_markup)
) )
logger.info( logger.info("User %s has been unblocked", fullclb[2])
app._(
"user_unblocked",
"console",
locale=app.config["locale_log"],
).format(fullclb[2]),
)

View File

@@ -1,3 +1,4 @@
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
@@ -18,16 +19,18 @@ async def cmd_kill(app: PyroClient, msg: Message):
if msg.from_user.id not in app.admins: if msg.from_user.id not in app.admins:
return return
user = await app.find_user(msg.from_user)
if len(USERS_WITH_CONTEXT) > 0: if len(USERS_WITH_CONTEXT) > 0:
await msg.reply_text( await msg.reply_text(
app._("shutdown_confirm", "message").format(len(USERS_WITH_CONTEXT)), app._("shutdown_confirm", "message", locale=user.locale).format(
len(USERS_WITH_CONTEXT)
),
reply_markup=InlineKeyboardMarkup( reply_markup=InlineKeyboardMarkup(
[ [
[ [
InlineKeyboardButton( InlineKeyboardButton(
app._( app._("shutdown", "button", locale=user.locale),
"shutdown", "button", locale=msg.from_user.language_code
),
callback_data="shutdown", callback_data="shutdown",
) )
] ]
@@ -42,4 +45,4 @@ async def cmd_kill(app: PyroClient, msg: Message):
Path(f"{app.config['locations']['cache']}/shutdown_time"), Path(f"{app.config['locations']['cache']}/shutdown_time"),
) )
exit() asyncio.get_event_loop().create_task(app.stop())

View File

@@ -3,22 +3,25 @@ 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 classes.user import PosterUser
@Client.on_message(~filters.scheduled & filters.command(["start"], prefixes="/")) @Client.on_message(~filters.scheduled & filters.command(["start"], prefixes="/"))
async def cmd_start(app: PyroClient, msg: Message): async def cmd_start(app: PyroClient, msg: Message):
if PosterUser(msg.from_user.id).is_blocked(): user = await app.find_user(msg.from_user)
if user.banned:
return return
await msg.reply_text(app._("start", "message", locale=msg.from_user.language_code)) await msg.reply_text(app._("start", "message", locale=user.locale))
@Client.on_message( @Client.on_message(
~filters.scheduled & filters.command(["rules", "help"], prefixes="/") ~filters.scheduled & filters.command(["rules", "help"], prefixes="/")
) )
async def cmd_rules(app: PyroClient, msg: Message): async def cmd_rules(app: PyroClient, msg: Message):
if PosterUser(msg.from_user.id).is_blocked(): user = await app.find_user(msg.from_user)
if user.banned:
return return
await msg.reply_text(app._("rules", "message", locale=msg.from_user.language_code)) await msg.reply_text(app._("rules", "message", locale=user.locale))

View File

@@ -13,16 +13,23 @@ 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 Message from pyrogram.types import (
KeyboardButton,
Message,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
)
from ujson import loads from ujson import loads
from classes.pyroclient import PyroClient from classes.pyroclient import PyroClient
from modules.api_client import ( from modules.api_client import (
BodyPhotoUploadAlbumsAlbumPhotosPost, BodyPhotoUpload,
File, File,
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
@@ -41,9 +48,9 @@ async def cmd_import(app: PyroClient, msg: Message):
else: else:
return return
await msg.reply_text( user = await app.find_user(msg.from_user)
app._("import_request", "message", locale=msg.from_user.language_code)
) await msg.reply_text(app._("import_request", "message", locale=user.locale))
answer = await listen_message(app, msg.chat.id, timeout=600) answer = await listen_message(app, msg.chat.id, timeout=600)
@@ -51,15 +58,13 @@ async def cmd_import(app: PyroClient, msg: Message):
if answer is None: if answer is None:
await msg.reply_text( await msg.reply_text(
app._("import_ignored", "message", locale=msg.from_user.language_code), app._("import_ignored", "message", locale=user.locale),
quote=True, quote=True,
) )
return return
if answer.text == "/cancel": if answer.text == "/cancel":
await answer.reply_text( await answer.reply_text(app._("import_abort", "message", locale=user.locale))
app._("import_abort", "message", locale=msg.from_user.language_code)
)
return return
if answer.document is None: if answer.document is None:
@@ -67,7 +72,7 @@ async def cmd_import(app: PyroClient, msg: Message):
app._( app._(
"import_invalid_media", "import_invalid_media",
"message", "message",
locale=msg.from_user.language_code, locale=user.locale,
), ),
quote=True, quote=True,
) )
@@ -75,16 +80,14 @@ async def cmd_import(app: PyroClient, msg: 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=msg.from_user.language_code), app._("import_invalid_mime", "message", locale=user.locale),
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 msg.reply_text( await msg.reply_text(
app._( app._("import_too_big", "message", locale=user.locale).format(
"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),
) )
@@ -104,14 +107,12 @@ async def cmd_import(app: PyroClient, msg: 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=msg.from_user.language_code), app._("import_downloading", "message", locale=user.locale),
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( await downloading.edit(app._("import_unpacking", "message", locale=user.locale))
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:
@@ -130,17 +131,15 @@ async def cmd_import(app: PyroClient, msg: Message):
format_exc(), format_exc(),
) )
await answer.reply_text( await answer.reply_text(
app._( app._("import_unpack_error", "message", locale=user.locale).format(
"import_unpack_error", "message", locale=msg.from_user.language_code exp, format_exc()
).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( await downloading.edit(app._("import_uploading", "message", locale=user.locale))
app._("import_uploading", "message", locale=msg.from_user.language_code)
)
remove(tmp_path) remove(tmp_path)
@@ -155,10 +154,11 @@ async def cmd_import(app: PyroClient, msg: 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,
multipart_data=BodyPhotoUploadAlbumsAlbumPhotosPost( 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"],
@@ -176,7 +176,7 @@ async def cmd_import(app: PyroClient, msg: Message):
app._( app._(
"import_upload_error_other", "import_upload_error_other",
"message", "message",
locale=msg.from_user.language_code, locale=user.locale,
).format(path.basename(filename)), ).format(path.basename(filename)),
disable_notification=True, disable_notification=True,
) )
@@ -197,7 +197,7 @@ async def cmd_import(app: PyroClient, msg: Message):
app._( app._(
"import_upload_error_duplicate", "import_upload_error_duplicate",
"message", "message",
locale=msg.from_user.language_code, locale=user.locale,
).format(path.basename(filename)), ).format(path.basename(filename)),
disable_notification=True, disable_notification=True,
) )
@@ -206,7 +206,7 @@ async def cmd_import(app: PyroClient, msg: Message):
app._( app._(
"import_upload_error_other", "import_upload_error_other",
"message", "message",
locale=msg.from_user.language_code, locale=user.locale,
).format(path.basename(filename)), ).format(path.basename(filename)),
disable_notification=True, disable_notification=True,
) )
@@ -227,7 +227,7 @@ async def cmd_import(app: PyroClient, msg: 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=msg.from_user.language_code), app._("import_finished", "message", locale=user.locale),
quote=True, quote=True,
) )
@@ -252,48 +252,101 @@ async def cmd_remove(app: PyroClient, msg: Message):
else: else:
return return
await msg.reply_text( user = await app.find_user(msg.from_user)
app._("remove_request", "message", locale=msg.from_user.language_code)
)
answer = await listen_message(app, msg.chat.id, timeout=600) await msg.reply_text(app._("remove_request", "message", locale=user.locale))
answer_id = await listen_message(app, msg.chat.id, timeout=600)
USERS_WITH_CONTEXT.remove(msg.from_user.id) USERS_WITH_CONTEXT.remove(msg.from_user.id)
if answer is None: if answer_id is None:
await msg.reply_text( await msg.reply_text(
app._("remove_ignored", "message", locale=msg.from_user.language_code), app._("remove_ignored", "message", locale=user.locale),
quote=True, quote=True,
) )
return return
if answer.text == "/cancel": if answer_id.text == "/cancel":
await answer.reply_text( await answer_id.reply_text(app._("remove_abort", "message", locale=user.locale))
app._("remove_abort", "message", locale=msg.from_user.language_code) return
await msg.reply_text(
app._("remove_kind", "message", locale=user.locale),
reply_markup=ReplyKeyboardMarkup(
[
[
KeyboardButton(app._("photo", "button", locale=user.locale)),
KeyboardButton(app._("video", "button", locale=user.locale)),
]
],
resize_keyboard=True,
one_time_keyboard=True,
),
)
USERS_WITH_CONTEXT.append(msg.from_user.id)
answer_kind = await listen_message(app, msg.chat.id, timeout=600)
USERS_WITH_CONTEXT.remove(msg.from_user.id)
if answer_kind is None:
await msg.reply_text(
app._("remove_ignored", "message", locale=user.locale),
quote=True,
reply_markup=ReplyKeyboardRemove(),
) )
return return
response = await photo_delete(id=answer.text, client=client) if answer_kind.text == "/cancel":
await answer_kind.reply_text(
if response: app._("remove_abort", "message", locale=user.locale),
logger.info( reply_markup=ReplyKeyboardRemove(),
"Removed '%s' by request of user %s", answer.text, answer.from_user.id
) )
await answer.reply_text( return
app._(
"remove_success", "message", locale=msg.from_user.language_code if answer_kind.text in app.in_all_locales("photo", "button"):
).format(answer.text) func = photo_delete
elif answer_kind.text in app.in_all_locales("video", "button"):
func = video_delete
else:
await answer_kind.reply_text(
app._("remove_unknown", "message", locale=user.locale).format(
app._("photo", "button", locale=user.locale),
app._("video", "button", locale=user.locale),
),
reply_markup=ReplyKeyboardRemove(),
)
return
response = await func(id=answer_id.text, client=client)
if response is None:
logger.info(
"Removed %s '%s' by request of user %s",
answer_kind.text,
answer_id.text,
answer_id.from_user.id,
)
await answer_kind.reply_text(
app._("remove_success", "message", locale=user.locale).format(
answer_id.text
),
reply_markup=ReplyKeyboardRemove(),
) )
else: else:
logger.warning( logger.warning(
"Could not remove '%s' by request of user %s", "Could not remove %s '%s' by request of user %s",
answer.text, answer_kind.text,
answer.from_user.id, answer_id.text,
answer_id.from_user.id,
) )
await answer.reply_text( await answer_kind.reply_text(
app._( app._("remove_failure", "message", locale=user.locale).format(
"remove_failure", "message", locale=msg.from_user.language_code answer_id.text
).format(answer.text) ),
reply_markup=ReplyKeyboardRemove(),
) )

View File

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

View File

@@ -13,8 +13,7 @@ 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 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__)
@@ -23,29 +22,34 @@ logger = logging.getLogger(__name__)
@Client.on_message( @Client.on_message(
~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, msg: Message): async def get_submission(app: PyroClient, msg: Message):
global USERS_WITH_CONTEXT global USERS_WITH_CONTEXT
if not hasattr(msg.from_user, "id"):
return
if msg.from_user.id in USERS_WITH_CONTEXT: if msg.from_user.id in USERS_WITH_CONTEXT:
return return
user = await app.find_user(msg.from_user)
user_owner = await app.find_user(app.owner)
try: try:
if col_banned.find_one({"user": msg.from_user.id}) is not None: if user.banned:
return return
await app.send_chat_action(msg.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 PosterUser(msg.from_user.id).is_limited(): if await user.is_limited():
await msg.reply_text( await msg.reply_text(
app._("sub_cooldown", "message", locale=user_locale).format( app._("sub_cooldown", "message", locale=user.locale).format(
str(app.config["submission"]["timeout"]) app.config["submission"]["timeout"]
) )
) )
return return
@@ -60,7 +64,7 @@ async def get_submission(app: PyroClient, msg: Message):
) )
if msg.document.mime_type not in app.config["submission"]["mime_types"]: if msg.document.mime_type not in app.config["submission"]["mime_types"]:
await msg.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,
@@ -68,8 +72,8 @@ async def get_submission(app: PyroClient, msg: Message):
return return
if msg.document.file_size > app.config["submission"]["file_size"]: if msg.document.file_size > app.config["submission"]["file_size"]:
await msg.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) app.config["submission"]["file_size"] / 1024 / 1024
), ),
quote=True, quote=True,
) )
@@ -90,8 +94,8 @@ async def get_submission(app: PyroClient, msg: Message):
) )
if msg.video.file_size > app.config["submission"]["file_size"]: if msg.video.file_size > app.config["submission"]["file_size"]:
await msg.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) app.config["submission"]["file_size"] / 1024 / 1024
), ),
quote=True, quote=True,
) )
@@ -100,27 +104,27 @@ async def get_submission(app: PyroClient, msg: Message):
save_tmp = False save_tmp = False
contents = msg.video.file_id, SubmissionType.VIDEO # , msg.video.file_name contents = msg.video.file_id, SubmissionType.VIDEO # , msg.video.file_name
if msg.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",
msg.from_user.id, # msg.from_user.id,
msg.animation.file_name, # msg.animation.file_name,
msg.animation.file_size / 1024 / 1024, # msg.animation.file_size / 1024 / 1024,
) # )
if msg.animation.file_size > app.config["submission"]["file_size"]: # if msg.animation.file_size > app.config["submission"]["file_size"]:
await msg.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 msg.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 = (
msg.animation.file_id, # msg.animation.file_id,
SubmissionType.ANIMATION, # SubmissionType.ANIMATION,
) # , msg.animation.file_name # ) # , msg.animation.file_name
if msg.photo is not None: if msg.photo is not None:
logger.info( logger.info(
@@ -176,7 +180,7 @@ async def get_submission(app: PyroClient, msg: Message):
buttons = [ buttons = [
[ [
InlineKeyboardButton( InlineKeyboardButton(
text=app._("sub_yes", "button"), text=app._("sub_yes", "button", locale=user_owner.locale),
callback_data=f"sub_yes_{str(inserted.inserted_id)}", callback_data=f"sub_yes_{str(inserted.inserted_id)}",
) )
] ]
@@ -186,7 +190,7 @@ async def get_submission(app: PyroClient, msg: Message):
caption = str(msg.caption) caption = str(msg.caption)
buttons[0].append( buttons[0].append(
InlineKeyboardButton( InlineKeyboardButton(
text=app._("sub_yes_caption", "button"), text=app._("sub_yes_caption", "button", locale=user_owner.locale),
callback_data=f"sub_yes_{str(inserted.inserted_id)}_caption", callback_data=f"sub_yes_{str(inserted.inserted_id)}_caption",
) )
) )
@@ -195,11 +199,11 @@ async def get_submission(app: PyroClient, msg: Message):
buttons[0].append( buttons[0].append(
InlineKeyboardButton( InlineKeyboardButton(
text=app._("sub_no", "button"), text=app._("sub_no", "button", locale=user_owner.locale),
callback_data=f"sub_no_{str(inserted.inserted_id)}", callback_data=f"sub_no_{str(inserted.inserted_id)}",
) )
) )
caption += app._("sub_by", "message") caption += app._("sub_by", "message", locale=user_owner.locale)
if msg.from_user.first_name is not None: if msg.from_user.first_name is not None:
caption += f" {msg.from_user.first_name}" caption += f" {msg.from_user.first_name}"
@@ -215,9 +219,9 @@ async def get_submission(app: PyroClient, msg: Message):
and app.config["submission"]["require_confirmation"]["admins"] is False and app.config["submission"]["require_confirmation"]["admins"] is False
): ):
try: try:
submitted = await app.submit_photo(str(inserted.inserted_id)) submitted = await app.submit_media(str(inserted.inserted_id))
await msg.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,
) )
@@ -227,7 +231,7 @@ async def get_submission(app: PyroClient, msg: Message):
return return
except SubmissionUnsupportedError: except SubmissionUnsupportedError:
await msg.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,
@@ -236,7 +240,7 @@ async def get_submission(app: PyroClient, msg: Message):
except SubmissionDuplicatesError as exp: except SubmissionDuplicatesError as exp:
await msg.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(exp.duplicates)), ).format("\n".join(exp.duplicates)),
quote=True, quote=True,
) )
@@ -251,7 +255,7 @@ async def get_submission(app: PyroClient, msg: Message):
try: try:
submitted = await app.submit_photo(str(inserted.inserted_id)) submitted = await app.submit_photo(str(inserted.inserted_id))
await msg.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,
) )
@@ -261,22 +265,22 @@ async def get_submission(app: PyroClient, msg: Message):
return return
except SubmissionUnsupportedError: except SubmissionUnsupportedError:
await msg.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 exp: except SubmissionDuplicatesError as exp:
await msg.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 exp: except Exception as exp:
await app.send_message( await app.send_message(
app.owner, app.owner,
app._("sub_error_admin", "message").format( app._(
msg.from_user.id, format_exc() "sub_error_admin", "message", locale=user_owner.locale
), ).format(msg.from_user.id, format_exc()),
) )
await msg.reply_text("sub_error", quote=True) await msg.reply_text("sub_error", quote=True)
return return
@@ -285,17 +289,17 @@ async def get_submission(app: PyroClient, msg: Message):
buttons += [ buttons += [
[ [
InlineKeyboardButton( InlineKeyboardButton(
text=app._("sub_block", "button"), text=app._("sub_block", "button", locale=user_owner.locale),
callback_data=f"sub_block_{msg.from_user.id}", callback_data=f"sub_block_{msg.from_user.id}",
) )
] ]
] ]
PosterUser(msg.from_user.id).limit() await user.update_cooldown()
if msg.from_user.id != app.owner: if msg.from_user.id != app.owner:
await msg.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,
) )

42
plugins/language.py Normal file
View File

@@ -0,0 +1,42 @@
from pykeyboard import InlineButton, InlineKeyboard
from pyrogram import filters
from pyrogram.client import Client
from pyrogram.types import CallbackQuery, Message
from classes.pyroclient import PyroClient
@Client.on_message(
~filters.scheduled & filters.private & filters.command(["language"], prefixes=["/"]) # type: ignore
)
async def command_language(app: PyroClient, message: Message):
user = await app.find_user(message.from_user)
keyboard = InlineKeyboard(row_width=2)
buttons = []
for locale, data in app.in_every_locale("metadata").items():
buttons.append(
InlineButton(f"{data['flag']} {data['name']}", f"language:{locale}")
)
keyboard.add(*buttons)
await message.reply_text(
app._("locale_choice", "message", locale=user.locale),
reply_markup=keyboard,
)
@Client.on_callback_query(filters.regex(r"language:[\s\S]*")) # type: ignore
async def callback_language(app: PyroClient, callback: CallbackQuery):
user = await app.find_user(callback.from_user)
language = str(callback.data).split(":")[1]
await user.update_locale(language)
await callback.answer(
app._("locale_set", "callback", locale=language).format(
locale=app._("name", "metadata", locale=language)
),
show_alert=True,
)

View File

@@ -1,17 +1,15 @@
aiofiles~=23.1.0
aiohttp~=3.8.4 aiohttp~=3.8.4
apscheduler~=3.10.1
black~=23.3.0 black~=23.3.0
convopyro==0.5 convopyro==0.5
pillow~=9.4.0 pillow~=10.0.0
psutil~=5.9.4 psutil~=5.9.4
pymongo~=4.3.3 pykeyboard==0.1.5
pymongo~=4.4.0
pyrogram==2.0.106 pyrogram==2.0.106
python_dateutil==2.8.2 python_dateutil==2.8.2
pytimeparse~=1.1.8 pytimeparse~=1.1.8
tgcrypto==1.2.5 tgcrypto==1.2.5
ujson==5.8.0
uvloop==0.17.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]==1.0 libbot[speed,pyrogram]==1.8
photosapi_client==0.2.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 poster.py python main.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 poster.py python main.py