113 Commits

Author SHA1 Message Date
f29c1e7ae6 WIP: Video support 2023-06-22 15:17:44 +02:00
070acb474d Bump PhotosAPI Client to 0.4.0 2023-06-22 14:56:43 +02:00
76855cd472 Mime type unsupported message added 2023-06-22 08:41:42 +00:00
592b83377b Sorted imports 2023-06-21 22:03:52 +02:00
7825d7ded3 Fixed missing IndexError handling 2023-06-21 21:43:23 +02:00
54af4399a6 Complete overhaul seems to be done 2023-06-21 19:28:17 +02:00
fc303ee127 WIP: Complete overhaul 2023-06-21 16:39:33 +02:00
bd62149a2c Update dependency ujson to v5.8.0 (#24)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ujson](https://github.com/ultrajson/ultrajson) | minor | `==5.7.0` -> `==5.8.0` |

---

### Release Notes

<details>
<summary>ultrajson/ultrajson</summary>

### [`v5.8.0`](https://github.com/ultrajson/ultrajson/releases/tag/5.8.0)

[Compare Source](https://github.com/ultrajson/ultrajson/compare/5.7.0...5.8.0)

#### Added

-   Build wheel for Python 3.12 beta (built against 3.12.0b2) ([#&#8203;594](https://github.com/ultrajson/ultrajson/issues/594)) [@&#8203;hugovk](https://github.com/hugovk)

#### Changed

-   Drop support for Python 3.7 ([#&#8203;595](https://github.com/ultrajson/ultrajson/issues/595)) [@&#8203;hugovk](https://github.com/hugovk)

#### Fixed

-   Include BSD-3-Clause and TCL license text ([#&#8203;584](https://github.com/ultrajson/ultrajson/issues/584)) [@&#8203;musicinmybrain](https://github.com/musicinmybrain)

</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/24
Co-authored-by: Renovate <renovate@noreply.localhost>
Co-committed-by: Renovate <renovate@noreply.localhost>
2023-06-11 12:29:02 +03:00
76fc4981cc Merge branch 'master' into dev 2023-05-16 15:45:11 +03:00
337a7b28aa Fixed coroutines not being awaited 2023-05-16 12:30:56 +02:00
93804345df Update dependency pyrogram to v2.0.106 (#22)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [pyrogram](https://github.com/pyrogram) ([source](https://github.com/pyrogram/pyrogram)) | patch | `==2.0.104` -> `==2.0.106` |

---

### Release Notes

<details>
<summary>pyrogram/pyrogram</summary>

### [`v2.0.106`](https://github.com/pyrogram/pyrogram/compare/v2.0.105...v2.0.106)

[Compare Source](https://github.com/pyrogram/pyrogram/compare/v2.0.105...v2.0.106)

### [`v2.0.105`](https://github.com/pyrogram/pyrogram/compare/v2.0.104...v2.0.105)

[Compare Source](https://github.com/pyrogram/pyrogram/compare/v2.0.104...v2.0.105)

</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-->

Reviewed-on: #22
Co-authored-by: Renovate <renovate@git.end-play.xyz>
Co-committed-by: Renovate <renovate@git.end-play.xyz>
2023-05-08 11:51:15 +03:00
f5b3335af0 master (#21)
Co-authored-by: Renovate <renovate@git.end-play.xyz>
Co-authored-by: profitroll <vozhd.kk@gmail.com>
Reviewed-on: #21
2023-04-24 14:38:03 +03:00
fc8f49b487 Commented random_pic_response output 2023-04-24 13:36:58 +02:00
c6cba6de2f Merge branch 'dev' 2023-04-24 13:33:33 +02:00
adef3b3afc Changed versioning prefix in code 2023-04-24 12:53:22 +02:00
fe60b3f8a5 Changed album config path 2023-04-24 12:44:10 +02:00
aa82a8f382 Fixed SyntaxError 2023-04-24 12:40:06 +02:00
826e031a39 Update dependency pyrogram to v2.0.104 (#18)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [pyrogram](https://github.com/pyrogram) ([source](https://github.com/pyrogram/pyrogram)) | patch | `==2.0.102` -> `==2.0.104` |

---

### Release Notes

<details>
<summary>pyrogram/pyrogram</summary>

### [`v2.0.104`](https://github.com/pyrogram/pyrogram/compare/v2.0.103...v2.0.104)

[Compare Source](https://github.com/pyrogram/pyrogram/compare/v2.0.103...v2.0.104)

### [`v2.0.103`](https://github.com/pyrogram/pyrogram/compare/v2.0.102...v2.0.103)

[Compare Source](https://github.com/pyrogram/pyrogram/compare/v2.0.102...v2.0.103)

</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-->

Reviewed-on: #18
Co-authored-by: Renovate <renovate@git.end-play.xyz>
Co-committed-by: Renovate <renovate@git.end-play.xyz>
2023-04-24 13:36:39 +03:00
0681338970 Update dependency psutil to v5.9.5 (#17)
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [psutil](https://github.com/giampaolo/psutil) | patch | `==5.9.4` -> `==5.9.5` |

---

### Release Notes

<details>
<summary>giampaolo/psutil</summary>

### [`v5.9.5`](https://github.com/giampaolo/psutil/blob/HEAD/HISTORY.rst#&#8203;595)

[Compare Source](https://github.com/giampaolo/psutil/compare/release-5.9.4...release-5.9.5)

\=====

2023-04-17

**Enhancements**

-   2196\_: in case of exception, display a cleaner error traceback by hiding the
    `KeyError` bit deriving from a missed cache hit.
-   2217\_: print the full traceback when a `DeprecationWarning` or `UserWarning`
    is raised.
-   2230\_, \[OpenBSD]: `psutil.net_connections`\_ implementation was rewritten from
    scratch:
    -   We're now able to retrieve the path of AF_UNIX sockets (before it was an
        empty string)
    -   The function is faster since it no longer iterates over all processes.
    -   No longer produces duplicate connection entries.
-   2238\_: there are cases where `Process.cwd()`\_ cannot be determined
    (e.g. directory no longer exists), in which case we returned either `None`
    or an empty string. This was consolidated and we now return `""` on all
    platforms.
-   2239\_, \[UNIX]: if process is a zombie, and we can only determine part of the
    its truncated `Process.name()`\_ (15 chars), don't fail with `ZombieProcess`\_
    when we try to guess the full name from the `Process.cmdline()`\_. Just
    return the truncated name.
-   2240\_, \[NetBSD], \[OpenBSD]: add CI testing on every commit for NetBSD and
    OpenBSD platforms (python 3 only).

**Bug fixes**

-   1043\_, \[OpenBSD] `psutil.net_connections`\_ returns duplicate entries.
-   1915\_, \[Linux]: on certain kernels, `"MemAvailable"` field from
    `/proc/meminfo` returns `0` (possibly a kernel bug), in which case we
    calculate an approximation for `available` memory which matches "free"
    CLI utility.
-   2164\_, \[Linux]: compilation fails on kernels < 2.6.27 (e.g. CentOS 5).
-   2186\_, \[FreeBSD]: compilation fails with Clang 15.  (patch by Po-Chuan Hsieh)
-   2191\_, \[Linux]: `disk_partitions()`*: do not unnecessarily read
    /proc/filesystems and raise `AccessDenied`* unless user specified `all=False`
    argument.
-   2216\_, \[Windows]: fix tests when running in a virtual environment (patch by
    Matthieu Darbois)
-   2225\_, \[POSIX]: `users()`\_ loses precision for `started` attribute (off by
    1 minute).
-   2229\_, \[OpenBSD]: unable to properly recognize zombie processes.
    `NoSuchProcess`\_ may be raised instead of `ZombieProcess`\_.
-   2231\_, \[NetBSD]: *available*  `virtual_memory()`\_ is higher than *total*.
-   2234\_, \[NetBSD]: `virtual_memory()`\_ metrics are wrong: *available* and
    *used* are too high. We now match values shown by *htop* CLI utility.
-   2236\_, \[NetBSD]: `Process.num_threads()`\_ and `Process.threads()`\_ return
    threads that are already terminated.
-   2237\_, \[OpenBSD], \[NetBSD]: `Process.cwd()`\_ may raise `FileNotFoundError`
    if cwd no longer exists. Return an empty string instead.

</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-->

Reviewed-on: #17
Co-authored-by: Renovate <renovate@git.end-play.xyz>
Co-committed-by: Renovate <renovate@git.end-play.xyz>
2023-04-24 13:36:12 +03:00
7c8e07bbc9 Made some dependencies strict 2023-04-24 12:13:14 +02:00
ad653146e1 Update dependency pillow to ~=9.5.0 (#15)
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)) | minor | `~=9.4.0` -> `~=9.5.0` |

---

### Release Notes

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

### [`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-->

Reviewed-on: https://git.end-play.xyz/profitroll/TelegramPoster/pulls/15
Co-authored-by: Renovate <renovate@git.end-play.xyz>
Co-committed-by: Renovate <renovate@git.end-play.xyz>
2023-04-20 16:04:39 +03:00
2b520760d3 Fixed branch name in Renovate config 2023-04-20 13:35:52 +02:00
9d340b32c1 Added Renovate config 2023-04-20 13:32:52 +02:00
4dd754055e Removed optional dependencies as a concept 2023-03-23 15:13:38 +01:00
ae654d686e TgCrypto is no longer optional dependency 2023-03-23 15:12:53 +01:00
af2bcd088f Album check returned 2023-03-22 15:52:34 +01:00
Profitroll
cc6523f604 Added updates checker 2023-03-22 11:03:03 +01:00
Profitroll
5ad52aa3f8 Changed aiohttp module location 2023-03-22 11:02:55 +01:00
Profitroll
45789ad013 Updates wiki init 2023-03-22 11:02:13 +01:00
Profitroll
4422a13ba9 Improved async context 2023-03-22 10:33:21 +01:00
f82c7b309f Added media removal 2023-03-21 14:34:25 +01:00
399fc5050d Updated user messages 2023-03-20 14:50:57 +01:00
56adb16a3e Replaced strings 2023-03-20 13:59:22 +01:00
1684c5177c Replaced /reboot with /shutdown 2023-03-20 13:56:18 +01:00
fba52bcfc4 Updated docs 2023-03-20 13:55:55 +01:00
20666fc0f8 Improved shutdown 2023-03-20 13:55:42 +01:00
03b6bbe039 Prototype: Files import 2023-03-20 12:03:03 +01:00
1749d49a52 Fixed enumerate error 2023-03-18 21:17:04 +01:00
a4323981fb WIP: Media import/export 2023-03-18 20:53:26 +01:00
06b6b49f43 This commit closes #13 2023-03-17 14:51:41 +01:00
1f345e87f7 Improved logging 2023-03-17 14:44:37 +01:00
717586a9f0 Improved logging 2023-03-17 14:13:28 +01:00
95351f247c This commit closed #12 2023-03-17 13:52:16 +01:00
b5e3abd4ad WIP: default config replacement for (#12) 2023-03-16 16:32:26 +01:00
4ec69c2a05 This commit closes #9 2023-03-16 15:03:14 +01:00
Profitroll
62e0a4986c WIP: ID returns 2023-03-16 12:58:57 +01:00
Profitroll
be8f0262d0 This commit closes #11 2023-03-15 21:58:14 +01:00
Profitroll
ba5a0f116c Merge branch 'dev' of https://git.end-play.xyz/profitroll/TelegramPoster into dev 2023-03-15 21:52:48 +01:00
Profitroll
e85adeafd0 Added info about venv to the README 2023-03-15 21:50:06 +01:00
87d2a2c6d3 Improved README 2023-03-13 13:02:06 +01:00
bc6080e832 Added venv guide to README 2023-03-13 11:33:30 +01:00
9bf4663b9e Made execution order correct 2023-03-12 22:58:55 +01:00
b74ce9ec89 Small fixes 2023-03-12 22:58:41 +01:00
6ed3673682 Fixed a typo 2023-03-12 22:45:06 +01:00
c593be156d Rearranged content 2023-03-12 22:30:35 +01:00
c96f98560c WIP: New readme 2023-03-12 22:29:53 +01:00
e1c1b58ffb Added warning about readme not being ready yet 2023-03-12 22:26:16 +01:00
bd2a1f0b12 WIP: New readme 2023-03-12 22:23:02 +01:00
8cc7808afb Disabled slash-escape for .json 2023-03-12 22:14:41 +01:00
6684c4d750 Bump Pyrogram to 2.0.102 2023-03-12 22:14:23 +01:00
c670db72fa Added user-friendly CLI 2023-03-12 22:14:03 +01:00
48acab2d5e WIP: New readme 2023-03-12 19:57:22 +01:00
62f076148c Wiki pages init 2023-03-12 19:43:51 +01:00
59b504e6d9 Temp: removed strings about API based in config 2023-03-12 19:34:27 +01:00
ad281ba981 WIP: New readme 2023-03-12 19:25:35 +01:00
0073120bf2 Moved ujson to required dependencies 2023-03-12 14:54:06 +01:00
244173e556 Bumped Pyrogram to 2.0.101 2023-03-12 14:25:22 +01:00
98e9c5f5a2 Fixed line being displayed incorrectly 2023-03-09 16:17:35 +01:00
a8545dd097 Fixed project name align 2023-03-09 16:16:38 +01:00
3f22340852 Added license and code style badges 2023-03-09 11:48:50 +01:00
c2fb88ed65 Bumped APScheduler to 3.10.1 2023-03-09 11:35:02 +01:00
88692ebc85 Now using black for formatting 2023-03-09 11:33:02 +01:00
4331af415e Moved to aiohttp and aiofiles 2023-03-02 22:38:48 +01:00
a913ba19c6 WIP: aiohttp migration 2023-03-02 16:39:59 +01:00
2387823151 Added missing texts 2023-02-25 23:31:09 +01:00
88f8bb4a52 WIP: Photos and queue removal 2023-02-25 23:19:08 +01:00
cf6c1f03d7 Disabled notification for accepted submissions 2023-02-25 23:12:33 +01:00
0a309a9f59 Added one more rule 2023-02-25 23:10:57 +01:00
7810f3b7b9 Fixed absence of None check 2023-02-24 21:06:18 +01:00
7b2534012d Fixed typo 2023-02-24 13:48:06 -05:00
6e8b47cf4b Improved submissions randomness 2023-02-24 13:33:34 -05:00
c27b1c5a5b Temporarily disabled album creation on start 2023-02-24 13:25:57 -05:00
c7228a006b Improved randomness 2023-02-24 13:25:36 -05:00
64ae3fb047 Added option to mention submissions 2023-02-24 12:39:33 -05:00
Profitroll
056fc52353 This commit closes #4 and closes #6 2023-02-19 20:54:58 +01:00
Profitroll
8bafd0cb35 This commit closes #3 2023-02-19 20:44:00 +01:00
Profitroll
fd47217bad This commit closes #5 2023-02-19 20:31:08 +01:00
Profitroll
6b7b5c22f2 Reverted web preview disable 2023-02-18 00:58:09 +01:00
Profitroll
b3698cfa70 Added support for duplicates access tokens 2023-02-18 00:55:58 +01:00
Profitroll
7607003f55 Added callback message for "nothing" 2023-02-17 23:45:55 +01:00
Profitroll
7918049f49 Fixed language code typo 2023-02-17 23:18:54 +01:00
Profitroll
fcd59b7aca Fixed caption display 2023-02-17 23:18:34 +01:00
Profitroll
8c478072c6 Fixed reply message not getting quoted 2023-02-17 23:18:19 +01:00
Profitroll
b766d0c52c Improved linting and removed unused imports 2023-02-17 22:59:03 +01:00
Profitroll
807e629ae7 Moved cooldowns to PosterUser class 2023-02-17 22:48:37 +01:00
Profitroll
87af9fd333 Fixed captions not getting detected 2023-02-17 22:26:07 +01:00
Profitroll
a54081a2ae Fixed some issues with locale 2023-02-17 21:58:46 +01:00
Profitroll
664284a6f8 Integrated previous commits 2023-02-17 21:55:38 +01:00
Profitroll
bd9917fb17 Replaced user block logic 2023-02-17 21:54:52 +01:00
Profitroll
68ea087963 Integrated interval-based schedule 2023-02-17 21:54:30 +01:00
Profitroll
d1813856d9 Imported callback "nothing" 2023-02-17 21:54:14 +01:00
Profitroll
cf204577e4 Added external address for links 2023-02-17 21:53:57 +01:00
Profitroll
68c887999e Divided admins and owner 2023-02-17 21:53:43 +01:00
Profitroll
642e17ee55 Image resize when too big 2023-02-17 16:46:44 +01:00
Profitroll
25af9b31f8 Changed Client to PosterClient 2023-02-17 16:46:33 +01:00
Profitroll
28fc359593 WIP: export and import 2023-02-17 16:46:13 +01:00
Profitroll
07203a9db9 Changed the way exceptions are handled 2023-02-17 16:45:51 +01:00
Profitroll
663a7b0db8 Submission without confirmation added 2023-02-17 16:44:56 +01:00
Profitroll
0d2e9fa6ec Using PosterClient instead of Client 2023-02-17 16:44:30 +01:00
Profitroll
c90e5eb697 Added Pillow to requirements 2023-02-17 16:44:03 +01:00
Profitroll
f4359aa6cd Added upload_pic method 2023-02-17 11:51:38 +01:00
4cd37be5dc WIP: New submission system 2023-02-16 16:41:01 +01:00
05042f01c6 Disabled WIP warning 2023-02-15 14:07:40 +01:00
30 changed files with 1548 additions and 1667 deletions

24
.gitignore vendored
View File

@@ -152,21 +152,13 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# ---> VisualStudioCode
.vscode/*
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Custom
config.json
*.session
*.session-journal
# Local History for Visual Studio Code
.history/
venv
venv_linux
venv_windows
# Built Visual Studio Code Extensions
*.vsix
# Project
cache
data
logs
config.json
.vscode

29
classes/commandset.py Normal file
View File

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

@@ -22,6 +22,11 @@ class SubmissionDuplicatesError(Exception):
)
class SubmissionUnsupportedError(Exception):
def __init__(self, file_path: str) -> None:
super().__init__(f"Type of file does not seem to be supported: '{file_path}'")
class UserCreationError(Exception):
def __init__(self, code: int, data: str) -> None:
self.code = code

View File

@@ -1,98 +0,0 @@
from os import path, remove, sep
from shutil import rmtree
from typing import Tuple, Union
from pyrogram.client import Client
from pyrogram.types import Message
from classes.exceptions import SubmissionDuplicatesError, SubmissionUnavailableError
from modules.api_client import upload_pic
from modules.database import col_submitted
from bson import ObjectId
from modules.logger import logWrite
from modules.utils import configGet
class PosterClient(Client):
def __init__(self, name: str, **kwargs): # type: ignore
super().__init__(name, **kwargs)
self.owner = configGet("owner")
self.admins = configGet("admins") + [configGet("owner")]
async def submit_photo(
self, id: str
) -> Tuple[Union[Message, None], Union[str, None]]:
db_entry = col_submitted.find_one({"_id": ObjectId(id)})
submission = None
if db_entry is None:
raise SubmissionUnavailableError()
else:
if db_entry["temp"]["uuid"] is not None:
if not path.exists(
path.join(
configGet("data", "locations"),
"submissions",
db_entry["temp"]["uuid"],
db_entry["temp"]["file"],
)
):
raise SubmissionUnavailableError()
else:
filepath = path.join(
configGet("data", "locations"),
"submissions",
db_entry["temp"]["uuid"],
db_entry["temp"]["file"],
)
try:
submission = await self.get_messages(
db_entry["user"], db_entry["telegram"]["msg_id"]
)
except:
pass
else:
try:
submission = await self.get_messages(
db_entry["user"], db_entry["telegram"]["msg_id"]
)
filepath = await self.download_media(
submission, file_name=configGet("tmp", "locations") + sep
)
except:
raise SubmissionUnavailableError()
response = await upload_pic(
str(filepath), allow_duplicates=configGet("allow_duplicates", "submission")
)
if len(response[1]) > 0:
raise SubmissionDuplicatesError(str(filepath), response[1])
col_submitted.find_one_and_update(
{"_id": ObjectId(id)}, {"$set": {"done": True}}
)
try:
if db_entry["temp"]["uuid"] is not None:
rmtree(
path.join(
configGet("data", "locations"),
"submissions",
db_entry["temp"]["uuid"],
),
ignore_errors=True,
)
else:
remove(str(filepath))
except (FileNotFoundError, NotADirectoryError):
logWrite(
f"Could not delete '{filepath}' on submission accepted", debug=True
)
return submission, response[2]
async def ban_user(self, id: int) -> None:
pass
async def unban_user(self, id: int) -> None:
pass

521
classes/pyroclient.py Normal file
View File

@@ -0,0 +1,521 @@
import contextlib
import logging
from datetime import datetime, timedelta
from io import BytesIO
from os import getpid, makedirs, remove, sep
from pathlib import Path
from shutil import rmtree
from time import time
from traceback import format_exc
from typing import List, Tuple, Union
import aiofiles
import pyrogram
from aiohttp import ClientSession
from bson import ObjectId
from dateutil.relativedelta import relativedelta
from classes.enums.submission_types import SubmissionType
from libbot import json_read, json_write
from libbot.i18n import BotLocale
from libbot.i18n.sync import _
from photosapi_client.errors import UnexpectedStatus
from pyrogram.client import Client
from pyrogram.errors import BadRequest, bad_request_400
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 ujson import dumps, loads
from classes.commandset import CommandSet
from classes.exceptions import (
SubmissionDuplicatesError,
SubmissionUnavailableError,
SubmissionUnsupportedError,
)
from classes.pyrocommand import PyroCommand
from modules.api_client import (
BodyPhotoUpload,
BodyVideoUpload,
File,
Photo,
Video,
client,
photo_upload,
video_upload,
)
from modules.database import col_submitted
from modules.http_client import http_session
from modules.scheduler import scheduler
from modules.sender import send_content
logger = logging.getLogger(__name__)
class PyroClient(Client):
def __init__(self):
with open("config.json", "r", encoding="utf-8") as f:
self.config: dict = loads(f.read())
super().__init__(
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.admins: List[int] = self.config["bot"]["admins"] + [
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()
async def start(self):
await super().start()
self.start_time = time()
logger.info(
"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 Path(f"{self.config['locations']['cache']}/shutdown_time").exists():
downtime = relativedelta(
datetime.now(),
datetime.fromtimestamp(
(
await json_read(
Path(
f"{self.config['locations']['cache']}/shutdown_time"
)
)
)["timestamp"]
),
)
if downtime.days >= 1:
startup_message = self._(
"startup_downtime_days",
"message",
).format(getpid(), downtime.days)
elif downtime.hours >= 1:
startup_message = self._(
"startup_downtime_hours",
"message",
).format(getpid(), downtime.hours)
else:
startup_message = self._(
"startup_downtime_minutes",
"message",
).format(getpid(), downtime.minutes)
else:
startup_message = (self._("startup", "message").format(getpid()),)
await self.send_message(
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,
"interval",
seconds=timeparse(self.config["posting"]["interval"]),
args=[self, self.sender_session],
)
else:
for entry in self.config["posting"]["time"]:
dt_obj = datetime.strptime(entry, "%H:%M")
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):
makedirs(self.config["locations"]["cache"], exist_ok=True)
await json_write(
{"timestamp": 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 self.sender_session.close()
await super().stop()
logger.warning("Bot stopped with PID %s.", getpid())
async def collect_commands(self) -> Union[List[CommandSet], None]:
"""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_media(
self, id: str
) -> Tuple[Union[Message, None], Union[str, None]]:
db_entry = col_submitted.find_one({"_id": ObjectId(id)})
submission = None
if db_entry is None:
raise SubmissionUnavailableError()
if db_entry["temp"]["uuid"] is None:
try:
submission = await self.get_messages(
db_entry["user"], db_entry["telegram"]["msg_id"]
)
filepath = await self.download_media(
submission, file_name=self.config["locations"]["tmp"] + sep
)
except Exception as exp:
raise SubmissionUnavailableError()
elif not Path(
f"{self.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}/{db_entry['temp']['file']}",
).exists():
raise SubmissionUnavailableError()
else:
filepath = Path(
f"{self.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}/{db_entry['temp']['file']}",
)
with contextlib.suppress(Exception):
submission = await self.get_messages(
db_entry["user"], db_entry["telegram"]["msg_id"]
)
async with aiofiles.open(str(filepath), "rb") as fh:
media_bytes = BytesIO(await fh.read())
try:
if db_entry["type"] == SubmissionType.PHOTO.value:
response = await photo_upload(
self.config["posting"]["api"]["album"],
client=client,
multipart_data=BodyPhotoUpload(
File(media_bytes, filepath.name, "image/jpeg")
),
ignore_duplicates=self.config["submission"]["allow_duplicates"],
compress=False,
caption="queue",
)
elif db_entry["type"] == SubmissionType.VIDEO.value:
response = await video_upload(
self.config["posting"]["api"]["album"],
client=client,
multipart_data=BodyVideoUpload(
File(media_bytes, filepath.name, "video/*")
),
caption="queue",
)
elif db_entry["type"] == SubmissionType.ANIMATION.value:
response = await video_upload(
self.config["posting"]["api"]["album"],
client=client,
multipart_data=BodyVideoUpload(
File(media_bytes, filepath.name, "video/*")
),
caption="queue",
)
except UnexpectedStatus:
raise SubmissionUnsupportedError(str(filepath))
response_dict = (
{}
if not hasattr(response, "content")
else loads(response.content.decode("utf-8"))
)
if "duplicates" in response_dict and len(response_dict["duplicates"]) > 0:
duplicates = []
for index, duplicate in enumerate(response_dict["duplicates"]): # type: ignore
if response_dict["access_token"] is None:
duplicates.append(
f"`{duplicate['id']}`:\n{self.config['posting']['api']['address_external']}/photos/{duplicate['id']}"
)
else:
duplicates.append(
f"`{duplicate['id']}`:\n{self.config['posting']['api']['address_external']}/token/photo/{response_dict['access_token']}?id={index}"
)
raise SubmissionDuplicatesError(str(filepath), duplicates)
col_submitted.find_one_and_update(
{"_id": ObjectId(id)}, {"$set": {"done": True}}
)
try:
if db_entry["temp"]["uuid"] is not None:
rmtree(
Path(
f"{self.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}",
),
ignore_errors=True,
)
else:
remove(str(filepath))
except (FileNotFoundError, NotADirectoryError):
logger.error("Could not delete '%s' on submission accepted", filepath)
return (
submission,
response.id if not hasattr(response, "parsed") else response.parsed.id,
)
async def ban_user(self, id: int) -> None:
pass
async def unban_user(self, id: int) -> None:
pass

9
classes/pyrocommand.py Normal file
View File

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

View File

@@ -1,7 +1,8 @@
from modules.app import app
from datetime import datetime
from libbot import sync
from modules.database import col_banned, col_users
from modules.utils import configGet
class PosterUser:
@@ -31,18 +32,20 @@ class PosterUser:
### Returns:
`bool`: Must be `True` if on the cooldown and `False` if not
"""
if self.id in app.admins:
if self.id in sync.config_get("admins", "bot"):
return False
else:
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()
< configGet("timeout", "submission")
else 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."""

View File

@@ -2,12 +2,14 @@
"locale": "en",
"locale_log": "en",
"locale_fallback": "en",
"owner": 0,
"admins": [],
"bot": {
"owner": 0,
"admins": [],
"api_id": 0,
"api_hash": "",
"bot_token": ""
"bot_token": "",
"max_concurrent_transmissions": 5,
"scoped_commands": true
},
"database": {
"user": null,
@@ -16,17 +18,18 @@
"port": 27017,
"name": "tgposter"
},
"mode": {
"post": true,
"submit": true
},
"reports": {
"chat_id": 0,
"sent": false,
"error": true,
"update": true,
"startup": true,
"shutdown": true
},
"mode": {
"post": true,
"submit": true
},
"logging": {
"size": 512,
"location": "logs"
@@ -40,6 +43,7 @@
"index": "data/index.json",
"locale": "locale"
},
"disabled_plugins": [],
"posting": {
"channel": 0,
"silent": false,
@@ -52,6 +56,11 @@
"ignore_admins": true,
"text": "#submitted"
},
"types": {
"photo": true,
"video": false,
"animation": false
},
"extensions": {
"photo": [
"jpg",
@@ -110,14 +119,76 @@
"video/quicktime"
]
},
"commands": [
"start",
"rules"
],
"commands_admin": [
"import",
"export",
"remove",
"shutdown"
]
"commands": {
"start": {
"scopes": [
{
"name": "BotCommandScopeDefault"
},
{
"name": "BotCommandScopeChat",
"chat_id": "owner"
}
]
},
"rules": {
"scopes": [
{
"name": "BotCommandScopeDefault"
},
{
"name": "BotCommandScopeChat",
"chat_id": "owner"
}
]
},
"forwards": {
"scopes": [
{
"name": "BotCommandScopeChat",
"chat_id": "owner"
}
]
},
"import": {
"scopes": [
{
"name": "BotCommandScopeChat",
"chat_id": "owner"
}
]
},
"export": {
"scopes": [
{
"name": "BotCommandScopeChat",
"chat_id": "owner"
}
]
},
"remove": {
"scopes": [
{
"name": "BotCommandScopeChat",
"chat_id": "owner"
}
]
},
"purge": {
"scopes": [
{
"name": "BotCommandScopeChat",
"chat_id": "owner"
}
]
},
"shutdown": {
"scopes": [
{
"name": "BotCommandScopeChat",
"chat_id": "owner"
}
]
}
}
}

View File

@@ -1,9 +1,7 @@
{
"commands": {
"start": "Start using the bot",
"rules": "Photos submission rules"
},
"commands_admin": {
"rules": "Photos submission rules",
"forwards": "Check post forwards",
"import": "Submit .zip archive with photos",
"export": "Get .zip archive with all photos",
@@ -60,7 +58,8 @@
"remove_abort": "Removal aborted.",
"remove_success": "Removed media with ID `{0}`.",
"remove_failure": "Could not remove media with ID `{0}`. Check if provided ID is correct and if it is - you can also check bot's log for details.",
"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."
},
"button": {
"sub_yes": "✅ Accept",
@@ -70,7 +69,8 @@
"sub_unblock": "🏳️ Unblock sender",
"post_view": "View in channel",
"accepted": "✅ Accepted",
"declined": "❌ Declined"
"declined": "❌ Declined",
"shutdown": "Confirm shutdown"
},
"callback": {
"sub_yes": "✅ Submission approved",

View File

@@ -1,9 +1,7 @@
{
"commands": {
"start": "Почати користуватись ботом",
"rules": "Правила пропонування фото"
},
"commands_admin": {
"rules": "Правила пропонування фото",
"forwards": "Переглянути репости",
"import": "Надати боту .zip архів з фотографіями",
"export": "Отримати .zip архів з усіма фотографіями",
@@ -60,7 +58,8 @@
"remove_abort": "Видалення перервано.",
"remove_success": "Видалено медіа з ID `{0}`.",
"remove_failure": "Не вдалося видалити медіа з ID `{0}`. Перевірте, чи вказано правильний ID, і якщо він правильний, ви також можете переглянути логи бота для отримання більш детальної інформації.",
"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} незавершених контекстів користувачів. Якщо ви вимкнете бота, вони будуть втрачені. Будь ласка, підтвердіть вимкнення за допомогою кнопки нижче."
},
"button": {
"sub_yes": "✅ Прийняти",
@@ -70,7 +69,8 @@
"sub_unblock": "🏳️ Розблокувати відправника",
"post_view": "Переглянути на каналі",
"accepted": "✅ Прийнято",
"declined": "❌ Відхилено"
"declined": "❌ Відхилено",
"shutdown": "Підтвердити вимкнення"
},
"callback": {
"sub_yes": "✅ Подання схвалено",

38
main.py Normal file
View File

@@ -0,0 +1,38 @@
import contextlib
import logging
from os import getpid
from convopyro import Conversation
from classes.pyroclient import PyroClient
from modules.scheduler import scheduler
logging.basicConfig(
level=logging.INFO,
format="%(name)s.%(funcName)s | %(levelname)s | %(message)s",
datefmt="[%X]",
)
logger = logging.getLogger(__name__)
with contextlib.suppress(ImportError):
import uvloop
uvloop.install()
def main():
client = PyroClient()
Conversation(client)
try:
client.run()
except KeyboardInterrupt:
logger.warning("Forcefully shutting down with PID %s...", getpid())
finally:
scheduler.shutdown()
exit()
if __name__ == "__main__":
main()

View File

@@ -1,38 +1,79 @@
"""This is only a temporary solution. Complete Photos API client is yet to be developed."""
import asyncio
import logging
from base64 import b64decode, b64encode
from os import makedirs, path, sep
from random import choice
from traceback import print_exc
from typing import Tuple, Union
from pathlib import Path
from typing import Union
import aiofiles
from aiohttp import FormData
from classes.exceptions import (
AlbumCreationDuplicateError,
AlbumCreationError,
AlbumCreationNameError,
SubmissionUploadError,
UserCreationDuplicateError,
UserCreationError,
from aiohttp import ClientSession
from libbot import config_get, i18n, sync
from photosapi_client import AuthenticatedClient, Client
from photosapi_client.api.default.album_create_albums_post import (
asyncio as album_create,
)
from modules.logger import logWrite
from modules.utils import configGet, locale
from photosapi_client.api.default.album_delete_album_id_delete import (
asyncio as album_delete,
)
from photosapi_client.api.default.album_find_albums_get import asyncio as album_find
from photosapi_client.api.default.login_for_access_token_token_post import sync as login
from photosapi_client.api.default.photo_delete_photos_id_delete import (
asyncio as photo_delete,
)
from photosapi_client.api.default.photo_find_albums_album_photos_get import (
asyncio as photo_find,
)
from photosapi_client.api.default.photo_get_photos_id_get import asyncio as photo_get
from photosapi_client.api.default.photo_patch_photos_id_patch import (
asyncio as photo_patch,
)
from photosapi_client.api.default.photo_upload_albums_album_photos_post import (
asyncio_detailed as photo_upload,
)
from photosapi_client.api.default.user_create_users_post import asyncio as user_create
from photosapi_client.api.default.user_me_users_me_get import sync as user_me
from photosapi_client.api.default.video_find_albums_album_videos_get import (
asyncio as video_find,
)
from photosapi_client.api.default.video_get_videos_id_get import asyncio as video_get
from photosapi_client.api.default.video_patch_videos_id_patch import (
asyncio as video_patch,
)
from photosapi_client.api.default.video_upload_albums_album_videos_post import (
asyncio as video_upload,
)
from photosapi_client.models.body_login_for_access_token_token_post import (
BodyLoginForAccessTokenTokenPost,
)
from photosapi_client.models.body_photo_upload_albums_album_photos_post import (
BodyPhotoUploadAlbumsAlbumPhotosPost as BodyPhotoUpload,
)
from photosapi_client.models.body_video_upload_albums_album_videos_post import (
BodyVideoUploadAlbumsAlbumVideosPost as BodyVideoUpload,
)
from photosapi_client.models.http_validation_error import HTTPValidationError
from photosapi_client.models.photo import Photo
from photosapi_client.models.token import Token
from photosapi_client.models.video import Video
from photosapi_client.types import File
from modules.http_client import http_session
logger = logging.getLogger(__name__)
async def authorize() -> str:
makedirs(configGet("cache", "locations"), exist_ok=True)
if path.exists(configGet("cache", "locations") + sep + "api_access") is True:
async def authorize(custom_session: Union[ClientSession, None] = None) -> str:
makedirs(await config_get("cache", "locations"), exist_ok=True)
session = http_session if custom_session is None else custom_session
if path.exists(await config_get("cache", "locations") + sep + "api_access") is True:
async with aiofiles.open(
configGet("cache", "locations") + sep + "api_access", "rb"
await config_get("cache", "locations") + sep + "api_access", "rb"
) as file:
token = b64decode(await file.read()).decode("utf-8")
if (
await http_session.get(
configGet("address", "posting", "api") + "/users/me/",
await session.get(
await config_get("address", "posting", "api") + "/users/me/",
headers={"Authorization": f"Bearer {token}"},
)
).status == 200:
@@ -40,27 +81,28 @@ async def authorize() -> str:
payload = {
"grant_type": "password",
"scope": "me albums.list albums.read albums.write photos.list photos.read photos.write videos.list videos.read videos.write",
"username": configGet("username", "posting", "api"),
"password": configGet("password", "posting", "api"),
"username": await config_get("username", "posting", "api"),
"password": await config_get("password", "posting", "api"),
}
response = await http_session.post(
configGet("address", "posting", "api") + "/token", data=payload
response = await session.post(
await config_get("address", "posting", "api") + "/token", data=payload
)
if not response.ok:
logWrite(
locale(
logger.warning(
i18n._(
"api_creds_invalid",
"console",
locale=configGet("locale_log").format(
configGet("address", "posting", "api"),
configGet("username", "posting", "api"),
locale=(await config_get("locale_log")).format(
await config_get("address", "posting", "api"),
await config_get("username", "posting", "api"),
response.status,
),
)
)
raise ValueError
async with aiofiles.open(
configGet("cache", "locations") + sep + "api_access", "wb"
str(Path(f"{await config_get('cache', 'locations')}/api_access")),
"wb",
) as file:
await file.write(
b64encode((await response.json())["access_token"].encode("utf-8"))
@@ -68,196 +110,36 @@ async def authorize() -> str:
return (await response.json())["access_token"]
async def random_pic(token: Union[str, None] = None) -> Tuple[str, str]:
"""Returns random image id and filename from the queue.
unauthorized_client = Client(
base_url=sync.config_get("address", "posting", "api"),
timeout=5.0,
verify_ssl=True,
raise_on_unexpected_status=True,
)
### Returns:
* `Tuple[str, str]`: First value is an ID and the filename in the filesystem to be indexed.
"""
token = await authorize() if token is None else token
logWrite(
f'{configGet("address", "posting", "api")}/albums/{configGet("album", "posting", "api")}/photos?q=&page_size={configGet("page_size", "posting")}&caption=queue'
login_token = login(
client=unauthorized_client,
form_data=BodyLoginForAccessTokenTokenPost(
grant_type="password",
scope="me albums.list albums.read albums.write photos.list photos.read photos.write videos.list videos.read videos.write",
username=sync.config_get("username", "posting", "api"),
password=sync.config_get("password", "posting", "api"),
),
)
if not isinstance(login_token, Token):
logger.warning(
"Could not initialize connection due to invalid token: %s", login_token
)
resp = await http_session.get(
f'{configGet("address", "posting", "api")}/albums/{configGet("album", "posting", "api")}/photos?q=&page_size={configGet("page_size", "posting")}&caption=queue',
headers={"Authorization": f"Bearer {token}"},
)
logWrite(
locale("random_pic_response", "console", locale=configGet("locale_log")).format(
await resp.json()
),
debug=True,
)
if resp.status != 200:
logWrite(
locale(
"random_pic_error_code",
"console",
locale=configGet("locale_log").format(
configGet("album", "posting", "api"), resp.status
),
),
)
logWrite(
locale(
"random_pic_error_debug",
"console",
locale=configGet("locale_log").format(
configGet("address", "posting", "api"),
configGet("album", "posting", "api"),
configGet("page_size", "posting"),
token,
resp.status,
),
),
debug=True,
)
raise ValueError
if len((await resp.json())["results"]) == 0:
raise KeyError
pic = choice((await resp.json())["results"])
return pic["id"], pic["filename"]
async def upload_pic(
filepath: str, allow_duplicates: bool = False, token: Union[str, None] = None
) -> Tuple[bool, list, Union[str, None]]:
token = await authorize() if token is None else token
try:
pic_name = path.basename(filepath)
logWrite(f"Uploading {pic_name} to the API...", debug=True)
async with aiofiles.open(filepath, "rb") as f:
file_bytes = await f.read()
formdata = FormData()
formdata.add_field(
"file", file_bytes, filename=pic_name, content_type="image/jpeg"
)
response = await http_session.post(
f'{configGet("address", "posting", "api")}/albums/{configGet("album", "posting", "api")}/photos',
params={
"caption": "queue",
"compress": "false",
"ignore_duplicates": str(allow_duplicates).lower(),
},
headers={"Authorization": f"Bearer {token}"},
data=formdata,
)
response_json = await response.json()
if response.status != 200 and response.status != 409:
logWrite(
locale(
"pic_upload_error",
"console",
locale=configGet("locale_log").format(
filepath, response.status, response.content
),
),
)
raise SubmissionUploadError(
str(filepath), response.status, response.content
)
id = response_json["id"] if "id" in await response.json() else None
duplicates = []
if "duplicates" in response_json:
for index, duplicate in enumerate(response_json["duplicates"]): # type: ignore
if response_json["access_token"] is None:
duplicates.append(
f'`{duplicate["id"]}`:\n{configGet("address_external", "posting", "api")}/photos/{duplicate["id"]}'
)
else:
duplicates.append(
f'`{duplicate["id"]}`:\n{configGet("address_external", "posting", "api")}/token/photo/{response_json["access_token"]}?id={index}'
)
return True, duplicates, id
except Exception as exp:
print_exc()
return False, [], None
async def find_pic(
name: str, caption: Union[str, None] = None, token: Union[str, None] = None
) -> Union[dict, None]:
token = await authorize() if token is None else token
try:
response = await http_session.get(
f'{configGet("address", "posting", "api")}/albums/{configGet("album", "posting", "api")}/photos',
params={"q": name, "caption": caption},
headers={"Authorization": f"Bearer {token}"},
)
# logWrite(response.json())
if response.status != 200:
return None
if len((await response.json())["results"]) == 0:
return None
return (await response.json())["results"]
except Exception as exp:
logWrite(
locale(
"find_pic_error",
"console",
locale=configGet("locale_log").format(name, caption, exp),
),
)
return None
async def move_pic(id: str, token: Union[str, None] = None) -> bool:
token = await authorize() if token is None else token
try:
response = await http_session.patch(
f'{configGet("address", "posting", "api")}/photos/{id}?caption=sent',
headers={"Authorization": f"Bearer {token}"},
)
if response.status != 200:
logWrite(f"Media moving failed with HTTP {response.status}", debug=True)
return False
return True
except:
return False
async def remove_pic(id: str, token: Union[str, None] = None) -> bool:
token = await authorize() if token is None else token
try:
response = await http_session.delete(
f'{configGet("address", "posting", "api")}/photos/{id}',
headers={"Authorization": f"Bearer {token}"},
)
if response.status != 204:
logWrite(f"Media removal failed with HTTP {response.status}", debug=True)
return False
return True
except:
return False
async def create_user(username: str, email: str, password: str) -> None:
response = await http_session.post(
f'{configGet("address", "posting", "api")}/users',
data={"user": username, "email": email, "password": password},
)
if response.status == 409:
raise UserCreationDuplicateError(username)
elif response.status != 204:
raise UserCreationError(response.status, await response.text(encoding="utf-8"))
return None
async def create_album(name: str, title: str) -> None:
token = await authorize()
response = await http_session.post(
f'{configGet("address", "posting", "api")}/albums',
params={"name": name, "title": title},
headers={"Authorization": f"Bearer {token}"},
)
if response.status == 409:
raise AlbumCreationDuplicateError(name)
elif response.status == 406:
raise AlbumCreationNameError(await response.json())
elif response.status != 200:
raise AlbumCreationError(response.status, await response.text(encoding="utf-8"))
return None
exit()
client = AuthenticatedClient(
base_url=sync.config_get("address", "posting", "api"),
timeout=5.0,
verify_ssl=True,
raise_on_unexpected_status=True,
token=login_token.access_token,
)
if __name__ == "__main__":
print(asyncio.run(authorize()))

View File

@@ -1,14 +0,0 @@
from modules.utils import configGet
from classes.poster_client import PosterClient
from convopyro import Conversation
app = PosterClient(
"duptsiaposter",
bot_token=configGet("bot_token", "bot"),
api_id=configGet("api_id", "bot"),
api_hash=configGet("api_hash", "bot"),
)
Conversation(app)
users_with_context = []

View File

@@ -1,78 +0,0 @@
import asyncio
from sys import exit
from traceback import print_exc
from modules.api_client import create_album, create_user, http_session
from argparse import ArgumentParser
from modules.utils import configSet
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()
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 create_user(username, email, password)
# asyncio.run(create_user(username, email, password))
configSet("username", username, "posting", "api")
configSet("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)
await http_session.close()
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:
result_2 = await create_album(name, title)
# asyncio.run(create_album(name, title))
configSet("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)
await http_session.close()
exit()
return None
if args.create_user or args.create_album:
loop = asyncio.get_event_loop()
tasks = []
if args.create_user:
loop.run_until_complete(asyncio.wait([loop.create_task(cli_create_user())]))
if args.create_album:
loop.run_until_complete(asyncio.wait([loop.create_task(cli_create_album())]))
loop.close()

View File

@@ -1,57 +0,0 @@
from os import listdir
from classes.poster_client import PosterClient
from pyrogram.types import BotCommand, BotCommandScopeChat
from modules.utils import configGet, locale
async def register_commands(app: PosterClient) -> None:
if configGet("submit", "mode"):
# Registering user commands
for entry in listdir(configGet("locale", "locations")):
if entry.endswith(".json"):
commands_list = []
for command in configGet("commands"):
commands_list.append(
BotCommand(
command,
locale(
command, "commands", locale=entry.replace(".json", "")
),
)
)
await app.set_bot_commands(
commands_list, language_code=entry.replace(".json", "")
)
# Registering user commands for fallback locale
commands_list = []
for command in configGet("commands"):
commands_list.append(
BotCommand(
command,
locale(command, "commands", locale=configGet("locale_fallback")),
)
)
await app.set_bot_commands(commands_list)
# Registering admin commands
commands_admin_list = []
if configGet("submit", "mode"):
for command in configGet("commands"):
commands_admin_list.append(
BotCommand(
command, locale(command, "commands", locale=configGet("locale"))
)
)
for command in configGet("commands_admin"):
commands_admin_list.append(
BotCommand(
command, locale(command, "commands_admin", locale=configGet("locale"))
)
)
for admin in app.admins:
await app.set_bot_commands(
commands_admin_list, scope=BotCommandScopeChat(chat_id=admin)
)

View File

@@ -1,67 +0,0 @@
from datetime import datetime
from gzip import open as gzipopen
from os import getcwd, makedirs, path, stat
from shutil import copyfileobj
from ujson import loads
with open(getcwd() + path.sep + "config.json", "r", encoding="utf8") as file:
json_contents = loads(file.read())
log_size = json_contents["logging"]["size"]
log_folder = json_contents["logging"]["location"]
file.close()
# Check latest log size
def checkSize(debug=False) -> None:
global log_folder
if debug:
log_file = "debug.log"
else:
log_file = "latest.log"
try:
makedirs(log_folder, exist_ok=True)
log = stat(path.join(log_folder, log_file))
if (log.st_size / 1024) > log_size:
with open(path.join(log_folder, log_file), "rb") as f_in:
with gzipopen(
path.join(
log_folder,
f'{datetime.now().strftime("%d.%m.%Y_%H:%M:%S")}.log.gz',
),
"wb",
) as f_out:
copyfileobj(f_in, f_out)
print(
f'Copied {path.join(log_folder, datetime.now().strftime("%d.%m.%Y_%H:%M:%S"))}.log.gz'
)
open(path.join(log_folder, log_file), "w").close()
except FileNotFoundError:
print(f"Log file {path.join(log_folder, log_file)} does not exist")
pass
# Append string to log
def logAppend(message, debug=False) -> None:
global log_folder
message_formatted = f'[{datetime.now().strftime("%d.%m.%Y")}] [{datetime.now().strftime("%H:%M:%S")}] {message}'
checkSize(debug=debug)
if debug:
log_file = "debug.log"
else:
log_file = "latest.log"
log = open(path.join(log_folder, log_file), "a")
log.write(f"{message_formatted}\n")
log.close()
# Print to stdout and then to log
def logWrite(message, debug=False) -> None:
# save to log file and rotation is to be done
logAppend(f"{message}", debug=debug)
print(f"{message}", flush=True)

View File

@@ -1,31 +1,3 @@
from datetime import datetime, timedelta
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from pytimeparse.timeparse import timeparse
from modules.utils import configGet
from modules.sender import send_content
from modules.commands_register import register_commands
from modules.app import app
scheduler = AsyncIOScheduler()
if configGet("post", "mode"):
if configGet("use_interval", "posting"):
scheduler.add_job(
send_content,
"interval",
seconds=timeparse(configGet("interval", "posting")),
args=[app],
)
else:
for entry in configGet("time", "posting"):
dt_obj = datetime.strptime(entry, "%H:%M")
scheduler.add_job(
send_content, "cron", hour=dt_obj.hour, minute=dt_obj.minute, args=[app]
)
scheduler.add_job(
register_commands,
"date",
run_date=datetime.now() + timedelta(seconds=10),
args=[app],
)

View File

@@ -1,113 +1,152 @@
import logging
from datetime import datetime
from os import makedirs, path
from random import choice
from shutil import rmtree
from traceback import format_exc
from traceback import format_exc, print_exc
from uuid import uuid4
from PIL import Image
import aiofiles
from aiohttp import ClientSession
from photosapi_client.errors import UnexpectedStatus
from PIL import Image
from pyrogram.client import Client
from classes.poster_client import PosterClient
from modules.api_client import authorize, move_pic, random_pic, http_session
from modules.api_client import (
authorize,
client,
photo_find,
photo_get,
photo_patch,
video_find,
video_get,
video_patch,
)
from modules.database import col_sent, col_submitted
from modules.logger import logWrite
from modules.utils import configGet, locale
logger = logging.getLogger(__name__)
async def send_content(app: PosterClient) -> None:
async def send_content(app: Client, http_session: ClientSession) -> None:
try:
try:
token = await authorize()
token = await authorize(http_session)
except ValueError:
await app.send_message(
app.owner,
locale("api_creds_invalid", "message", locale=configGet("locale")),
app._("api_creds_invalid", "message"),
)
return
try:
pic = await random_pic()
except KeyError:
logWrite(locale("post_empty", "console", locale=configGet("locale")))
if configGet("error", "reports"):
await app.send_message(
app.owner,
locale("api_queue_empty", "message", locale=configGet("locale")),
)
return
except ValueError:
if configGet("error", "reports"):
await app.send_message(
app.owner,
locale("api_queue_error", "message", locale=configGet("locale")),
)
return
funcs = []
if app.config["posting"]["types"]["photo"]:
funcs.append(photo_find)
if (
app.config["posting"]["types"]["video"]
or app.config["posting"]["types"]["animation"]
):
funcs.append(video_find)
func = choice(funcs)
response = await http_session.get(
f'{configGet("address", "posting", "api")}/photos/{pic[0]}',
headers={"Authorization": f"Bearer {token}"},
)
if response.status != 200:
logWrite(
locale(
"post_invalid_pic", "console", locale=configGet("locale")
).format(response.status, str(response.json()))
media = choice(
(
await func(
album=app.config["posting"]["api"]["album"],
caption="queue",
page_size=app.config["posting"]["page_size"],
client=client,
)
).results
)
if configGet("error", "reports"):
except (KeyError, AttributeError, TypeError, IndexError):
logger.info(app._("post_empty", "console"))
if app.config["reports"]["error"]:
await app.send_message(
app.owner,
locale(
"post_invalid_pic", "message", locale=configGet("locale")
).format(response.status, response.json()),
app._("api_queue_empty", "message"),
)
return
except (ValueError, UnexpectedStatus):
if app.config["reports"]["error"]:
await app.send_message(
app.owner,
app._("api_queue_error", "message"),
)
return
try:
if func is photo_find:
response = await photo_get(id=media.id, client=client)
else:
response = await video_get(id=media.id, client=client)
except Exception as exp:
print_exc()
logger.warning(
"Media is invalid: %s",
exp
# app._("post_invalid_pic", "console").format(
# response.status, str(await response.json())
# )
)
if app.config["reports"]["error"]:
await app.send_message(app.owner, f"Media is invalid: {exp}")
return
# await app.send_message(
# app.owner,
# app._("post_invalid_pic", "message").format(
# response.status, await response.json()
# ),
# )
tmp_dir = str(uuid4())
makedirs(path.join(configGet("tmp", "locations"), tmp_dir), exist_ok=True)
makedirs(path.join(app.config["locations"]["tmp"], tmp_dir), exist_ok=True)
tmp_path = path.join(tmp_dir, pic[1])
tmp_path = path.join(tmp_dir, media.filename)
async with aiofiles.open(
path.join(configGet("tmp", "locations"), tmp_path), "wb"
path.join(app.config["locations"]["tmp"], tmp_path), "wb"
) as out_file:
await out_file.write(await response.read())
await out_file.write(response.payload.read())
logWrite(
f'Candidate {pic[1]} ({pic[0]}) is {path.getsize(path.join(configGet("tmp", "locations"), tmp_path))} bytes big',
debug=True,
logger.info(
f"Candidate {media.filename} ({media.id}) is {path.getsize(path.join(app.config['locations']['tmp'], tmp_path))} bytes big",
)
if path.getsize(path.join(configGet("tmp", "locations"), tmp_path)) > 5242880:
image = Image.open(path.join(configGet("tmp", "locations"), tmp_path))
if (
path.getsize(path.join(app.config["locations"]["tmp"], tmp_path)) > 5242880
) and func is photo_find:
image = Image.open(path.join(app.config["locations"]["tmp"], tmp_path))
width, height = image.size
image = image.resize((int(width / 2), int(height / 2)), Image.ANTIALIAS)
if tmp_path.lower().endswith(".jpeg") or tmp_path.lower().endswith(".jpg"):
image.save(
path.join(configGet("tmp", "locations"), tmp_path),
path.join(app.config["locations"]["tmp"], tmp_path),
"JPEG",
optimize=True,
quality=50,
)
elif tmp_path.lower().endswith(".png"):
image.save(
path.join(configGet("tmp", "locations"), tmp_path),
path.join(app.config["locations"]["tmp"], tmp_path),
"PNG",
optimize=True,
compress_level=8,
)
image.close()
if path.getsize(path.join(configGet("tmp", "locations"), tmp_path)) > 5242880:
if (
path.getsize(path.join(app.config["locations"]["tmp"], tmp_path)) > 5242880
) and func is photo_find:
rmtree(
path.join(configGet("tmp", "locations"), tmp_dir), ignore_errors=True
path.join(app.config["locations"]["tmp"], tmp_dir), ignore_errors=True
)
raise BytesWarning
del response
submitted = col_submitted.find_one({"temp.file": pic[1]})
submitted = col_submitted.find_one({"temp.file": media.filename})
if submitted is not None and submitted["caption"] is not None:
caption = submitted["caption"].strip()
@@ -116,86 +155,91 @@ async def send_content(app: PosterClient) -> None:
if (
submitted is not None
and configGet("enabled", "posting", "submitted_caption")
and app.config["posting"]["submitted_caption"]["enabled"]
and (
(submitted["user"] not in app.admins)
or (configGet("ignore_admins", "posting", "submitted_caption") is False)
or (
app.config["posting"]["submitted_caption"]["ignore_admins"] is False
)
)
):
caption = (
f"{caption}\n\n{configGet('text', 'posting', 'submitted_caption')}\n"
f"{caption}\n\n{app.config['posting']['submitted_caption']['text']}\n"
)
else:
caption = f"{caption}\n\n"
if configGet("enabled", "caption"):
if configGet("link", "caption") != None:
caption = f"{caption}[{choice(configGet('text', 'caption'))}]({configGet('link', 'caption')})"
if app.config["caption"]["enabled"]:
if app.config["caption"]["link"] is not None:
caption = f"{caption}[{choice(app.config['caption']['text'])}]({app.config['caption']['link']})"
else:
caption = f"{caption}{choice(configGet('text', 'caption'))}"
caption = f"{caption}{choice(app.config['caption']['text'])}"
else:
caption = caption
try:
sent = await app.send_photo(
configGet("channel", "posting"),
path.join(configGet("tmp", "locations"), tmp_path),
caption=caption,
disable_notification=configGet("silent", "posting"),
)
if func is photo_find:
sent = await app.send_photo(
app.config["posting"]["channel"],
path.join(app.config["locations"]["tmp"], tmp_path),
caption=caption,
disable_notification=app.config["posting"]["silent"],
)
else:
sent = await app.send_video(
app.config["posting"]["channel"],
path.join(app.config["locations"]["tmp"], tmp_path),
caption=caption,
disable_notification=app.config["posting"]["silent"],
)
except Exception as exp:
logWrite(f"Could not send image {pic[1]} ({pic[0]}) due to {exp}")
if configGet("error", "reports"):
logger.error(
f"Could not send media {media.filename} ({media.id}) due to {exp}"
)
if app.config["reports"]["error"]:
await app.send_message(
app.owner,
locale(
"post_exception", "message", locale=configGet("locale")
).format(exp, format_exc()),
app._("post_exception", "message").format(exp, format_exc()),
)
# rmtree(path.join(configGet("tmp", "locations"), tmp_dir), ignore_errors=True)
# rmtree(path.join(app.config['locations']['tmp'], tmp_dir), ignore_errors=True)
return
col_sent.insert_one(
{
"date": datetime.now(),
"image": pic[0],
"filename": pic[1],
"channel": configGet("channel", "posting"),
"image": media.id,
"filename": media.filename,
"channel": app.config["posting"]["channel"],
"caption": None
if (submitted is None or submitted["caption"] is None)
else submitted["caption"].strip(),
}
)
await move_pic(pic[0])
func_patch = photo_patch if func is photo_find else video_patch
await func_patch(id=media.id, client=client, caption="sent")
rmtree(path.join(configGet("tmp", "locations"), tmp_dir), ignore_errors=True)
rmtree(path.join(app.config["locations"]["tmp"], tmp_dir), ignore_errors=True)
logWrite(
locale("post_sent", "console", locale=configGet("locale")).format(
pic[0],
str(configGet("channel", "posting")),
logger.info(
app._("post_sent", "console").format(
media.id,
str(app.config["posting"]["channel"]),
caption.replace("\n", "%n"),
str(configGet("silent", "posting")),
str(app.config["posting"]["silent"]),
)
)
except Exception as exp:
logWrite(
locale("post_exception", "console", locale=configGet("locale")).format(
str(exp), format_exc()
)
)
if configGet("error", "reports"):
logger.error(app._("post_exception", "console").format(str(exp), format_exc()))
if app.config["reports"]["error"]:
await app.send_message(
app.owner,
locale("post_exception", "message", locale=configGet("locale")).format(
exp, format_exc()
),
app._("post_exception", "message").format(exp, format_exc()),
)
try:
rmtree(
path.join(configGet("tmp", "locations"), tmp_dir), ignore_errors=True
path.join(app.config["locations"]["tmp"], tmp_dir), ignore_errors=True
)
except:
pass

View File

@@ -1,252 +1,33 @@
from os import kill, makedirs
from os import name as osname
from os import path, sep
from sys import exit
from traceback import print_exc
from typing import Any
import logging
from os import makedirs, path
from pathlib import Path
from typing import List, Union
from zipfile import ZipFile
import aiofiles
from ujson import JSONDecodeError, dumps, loads
from modules.logger import logWrite
logger = logging.getLogger(__name__)
default_config = {
"locale": "en",
"locale_log": "en",
"locale_fallback": "en",
"owner": 0,
"admins": [],
"bot": {"api_id": 0, "api_hash": "", "bot_token": ""},
"database": {
"user": None,
"password": None,
"host": "127.0.0.1",
"port": 27017,
"name": "tgposter",
},
"mode": {"post": True, "submit": True},
"reports": {"sent": False, "error": True, "startup": True, "shutdown": True},
"logging": {"size": 512, "location": "logs"},
"locations": {
"tmp": "tmp",
"data": "data",
"cache": "cache",
"sent": "data/sent",
"queue": "data/queue",
"index": "data/index.json",
"locale": "locale",
},
"posting": {
"channel": 0,
"silent": False,
"move_sent": False,
"use_interval": False,
"interval": "1h30m",
"page_size": 300,
"submitted_caption": {
"enabled": True,
"ignore_admins": True,
"text": "#submitted",
},
"extensions": {
"photo": ["jpg", "png", "gif", "jpeg"],
"video": ["mp4", "avi", "mkv", "webm", "mov"],
},
"time": [
"08:00",
"10:00",
"12:00",
"14:00",
"16:00",
"18:00",
"20:00",
"22:00",
],
"api": {
"address": "http://localhost:8054",
"address_external": "https://photos.domain.com",
"username": "",
"password": "",
"album": "",
},
},
"caption": {"enabled": False, "link": None, "text": ["sample text"]},
"submission": {
"timeout": 30,
"file_size": 15728640,
"tmp_size": 15728640,
"allow_duplicates": False,
"send_uploaded_id": False,
"require_confirmation": {"users": True, "admins": True},
"mime_types": [
"image/png",
"image/gif",
"image/jpeg",
"video/mp4",
"video/quicktime",
],
},
"commands": ["start", "rules"],
"commands_admin": ["import", "export", "shutdown"],
}
USERS_WITH_CONTEXT: List[int] = []
def jsonLoad(filename: str) -> Any:
"""Loads arg1 as json and returns its contents"""
with open(filename, "r", encoding="utf8") as file:
try:
output = loads(file.read())
except JSONDecodeError:
logWrite(
f"Could not load json file {filename}: file seems to be incorrect!\n{print_exc()}"
)
raise
except FileNotFoundError:
logWrite(
f"Could not load json file {filename}: file does not seem to exist!\n{print_exc()}"
)
raise
file.close()
return output
def jsonSave(contents: Any, filename: str) -> None:
"""Dumps dict/list arg1 to file arg2"""
try:
with open(filename, "w", encoding="utf8") as file:
file.write(
dumps(
contents, ensure_ascii=False, indent=4, escape_forward_slashes=False
)
)
file.close()
except Exception as exp:
logWrite(f"Could not save json file {filename}: {exp}\n{print_exc()}")
return
def configSet(key: str, value, *args: str):
"""Set key to a value
Args:
* key (str): The last key of the keys path.
* value (str/int/float/list/dict/None): Some needed value.
* *args (str): Path to key like: dict[args][key].
"""
this_dict = jsonLoad("config.json")
string = "this_dict"
for arg in args:
string += f'["{arg}"]'
if type(value) in [str]:
string += f'["{key}"] = "{value}"'
else:
string += f'["{key}"] = {value}'
exec(string)
jsonSave(this_dict, "config.json")
return
def configGet(key: str, *args: str):
"""Get value of the config key
Args:
* key (str): The last key of the keys path.
* *args (str): Path to key like: dict[args][key].
Returns:
* any: Value of provided key
"""
this_dict = jsonLoad("config.json")
try:
this_key = this_dict
for dict_key in args:
this_key = this_key[dict_key]
this_key[key]
except KeyError:
print(
f"Could not find config key '{key}' under path {args}: falling back to default config",
flush=True,
)
this_key = default_config
for dict_key in args:
this_key = this_key[dict_key]
configSet(key, this_key[key], *args)
return this_key[key]
def locale(key: str, *args: str, locale=configGet("locale")):
"""Get value of locale string
Args:
* key (str): The last key of the locale's keys path.
* *args (list): Path to key like: dict[args][key].
* locale (str): Locale to looked up in. Defaults to config's locale value.
Returns:
* any: Value of provided locale key
"""
if locale == None:
locale = configGet("locale")
try:
this_dict = jsonLoad(f'{configGet("locale", "locations")}{sep}{locale}.json')
except FileNotFoundError:
try:
this_dict = jsonLoad(
f'{configGet("locale", "locations")}{sep}{configGet("locale")}.json'
)
except FileNotFoundError:
try:
this_dict = jsonLoad(
f'{configGet("locale_fallback", "locations")}{sep}{configGet("locale")}.json'
)
except:
return f'⚠️ Locale in config is invalid: could not get "{key}" in {str(args)} from locale "{locale}"'
this_key = this_dict
for dict_key in args:
this_key = this_key[dict_key]
try:
return this_key[key]
except KeyError:
return f'⚠️ Locale in config is invalid: could not get "{key}" in {str(args)} from locale "{locale}"'
async def extract_and_save(handle: ZipFile, filename: str, destpath: str):
async def extract_and_save(handle: ZipFile, filename: str, destpath: Union[str, Path]):
"""Extract and save file from archive
Args:
* handle (ZipFile): ZipFile handler
* filename (str): File base name
* path (str): Path where to store
### Args:
* handle (`ZipFile`): ZipFile handler
* filename (`str`): File base name
* path (`Union[str, Path]`): Path where to store
"""
data = handle.read(filename)
filepath = path.join(destpath, filename)
filepath = path.join(str(destpath), filename)
try:
makedirs(path.dirname(filepath), exist_ok=True)
async with aiofiles.open(filepath, "wb") as fd:
await fd.write(data)
logWrite(f"Unzipped {filename}", debug=True)
logger.debug("Unzipped %s", filename)
except IsADirectoryError:
makedirs(filepath, exist_ok=True)
except FileNotFoundError:
pass
return
try:
from psutil import Process
except ModuleNotFoundError:
print(locale("deps_missing", "console", locale=configGet("locale")), flush=True)
exit()
def killProc(pid: int) -> None:
"""Kill process by its PID. Meant to be used to kill the main process of bot itself.
### Args:
* pid (`int`): PID of the target
"""
if osname == "posix":
from signal import SIGKILL
kill(pid, SIGKILL)
else:
Process(pid).kill()

View File

@@ -1,12 +1,12 @@
from modules.app import app
from pyrogram import filters
from pyrogram.client import Client
from pyrogram.types import CallbackQuery
from classes.poster_client import PosterClient
from modules.utils import locale
from classes.pyroclient import PyroClient
@app.on_callback_query(filters.regex("nothing"))
async def callback_query_nothing(app: PosterClient, clb: CallbackQuery):
@Client.on_callback_query(filters.regex("nothing"))
async def callback_query_nothing(app: PyroClient, clb: CallbackQuery):
await clb.answer(
text=locale("nothing", "callback", locale=clb.from_user.language_code)
text=app._("nothing", "callback", locale=clb.from_user.language_code)
)

View File

@@ -1,29 +1,25 @@
from os import getpid, makedirs, path
from os import makedirs, path
from time import time
from modules.app import app
from libbot import config_get, json_write
from pyrogram import filters
from pyrogram.client import Client
from pyrogram.types import CallbackQuery
from classes.poster_client import PosterClient
from modules.scheduler import scheduler
from modules.logger import logWrite
from modules.utils import configGet, jsonSave, locale
from classes.pyroclient import PyroClient
@app.on_callback_query(filters.regex("shutdown"))
async def callback_query_nothing(app: PosterClient, clb: CallbackQuery):
if clb.from_user.id in app.admins:
pid = getpid()
logWrite(f"Shutting down bot with pid {pid}")
await clb.answer()
await clb.message.reply_text(
locale("shutdown", "message", locale=clb.from_user.language_code).format(
pid
),
)
scheduler.shutdown()
makedirs(configGet("cache", "locations"), exist_ok=True)
jsonSave(
{"timestamp": time()},
path.join(configGet("cache", "locations"), "shutdown_time"),
)
exit()
@Client.on_callback_query(filters.regex("shutdown"))
async def callback_query_nothing(app: PyroClient, clb: CallbackQuery):
if clb.from_user.id not in app.admins:
return
await clb.answer()
makedirs(await config_get("cache", "locations"), exist_ok=True)
await json_write(
{"timestamp": time()},
path.join(await config_get("cache", "locations"), "shutdown_time"),
)
exit()

View File

@@ -1,20 +1,28 @@
import logging
from os import path
from pathlib import Path
from shutil import rmtree
from pyrogram import filters
from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from classes.exceptions import SubmissionDuplicatesError, SubmissionUnavailableError
from classes.poster_client import PosterClient
from classes.user import PosterUser
from modules.app import app
from modules.logger import logWrite
from modules.utils import configGet, locale
from modules.database import col_submitted
from bson import ObjectId
from libbot import config_get
from pyrogram import filters
from pyrogram.client import Client
from pyrogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from classes.exceptions import (
SubmissionDuplicatesError,
SubmissionUnavailableError,
SubmissionUnsupportedError,
)
from classes.pyroclient import PyroClient
from classes.user import PosterUser
from modules.database import col_submitted
logger = logging.getLogger(__name__)
@app.on_callback_query(filters.regex("sub_yes_[\s\S]*"))
async def callback_query_yes(app: PosterClient, clb: CallbackQuery):
@Client.on_callback_query(filters.regex("sub_yes_[\s\S]*"))
async def callback_query_yes(app: PyroClient, clb: CallbackQuery):
fullclb = str(clb.data).split("_")
user_locale = clb.from_user.language_code
@@ -24,44 +32,51 @@ async def callback_query_yes(app: PosterClient, clb: CallbackQuery):
submission = await app.submit_photo(fullclb[2])
except SubmissionUnavailableError:
await clb.answer(
text=locale("sub_msg_unavail", "callback", locale=user_locale),
text=app._("sub_msg_unavail", "callback", locale=user_locale),
show_alert=True,
)
return
except SubmissionUnsupportedError:
await clb.answer(
text=app._("mime_not_allowed", "message", locale=user_locale).format(
", ".join(app.config["submission"]["mime_types"]), quote=True
),
show_alert=True,
)
return
except SubmissionDuplicatesError as exp:
await clb.answer(
text=locale("sub_duplicates_found", "callback", locale=user_locale),
text=app._("sub_duplicates_found", "callback", locale=user_locale),
show_alert=True,
)
await clb.message.reply_text(
locale("sub_media_duplicates_list", "message", locale=user_locale).format(
app._("sub_media_duplicates_list", "message", locale=user_locale).format(
"\n".join(exp.duplicates)
),
quote=True,
)
logWrite(
locale(
logger.info(
app._(
"submission_duplicate",
"console",
locale=configGet("locale_log").format(
fullclb[2],
str(exp.duplicates),
),
locale=app.config["locale_log"],
).format(
fullclb[2],
str(exp.duplicates),
),
debug=True,
)
return
if submission[0] is not None:
await submission[0].reply_text(
locale("sub_yes", "message", locale=submission[0].from_user.language_code),
app._("sub_yes", "message", locale=submission[0].from_user.language_code),
quote=True,
)
elif db_entry is not None:
await app.send_message(db_entry["user"], locale("sub_yes", "message"))
await app.send_message(db_entry["user"], app._("sub_yes", "message"))
await clb.answer(
text=locale("sub_yes", "callback", locale=user_locale).format(fullclb[2]),
text=app._("sub_yes", "callback", locale=user_locale).format(fullclb[2]),
show_alert=True,
)
@@ -69,7 +84,7 @@ async def callback_query_yes(app: PosterClient, clb: CallbackQuery):
[
[
InlineKeyboardButton(
text=str(locale("accepted", "button", locale=user_locale)),
text=str(app._("accepted", "button", locale=user_locale)),
callback_data="nothing",
)
],
@@ -79,14 +94,14 @@ async def callback_query_yes(app: PosterClient, clb: CallbackQuery):
else [
[
InlineKeyboardButton(
text=str(locale("accepted", "button", locale=user_locale)),
text=str(app._("accepted", "button", locale=user_locale)),
callback_data="nothing",
)
]
]
)
if configGet("send_uploaded_id", "submission"):
if await config_get("send_uploaded_id", "submission"):
await clb.message.edit_caption(
clb.message.caption + f"\n\nID: `{submission[1]}`"
)
@@ -95,79 +110,52 @@ async def callback_query_yes(app: PosterClient, clb: CallbackQuery):
reply_markup=InlineKeyboardMarkup(edited_markup)
)
logWrite(
locale(
logger.info(
app._(
"submission_accepted",
"console",
locale=configGet("locale_log").format(fullclb[2], submission[1]),
),
debug=True,
locale=app.config["locale_log"],
).format(fullclb[2], submission[1]),
)
# try:
# if configGet("api_based", "mode") is True:
# media = await app.download_media(submission, file_name=configGet("tmp", "locations")+sep)
# upload = upload_pic(media)
# if upload[0] is False:
# await clb.answer(text=locale("sub_media_failed", "message", locale=user_locale), show_alert=True)
# elif len(upload[1]) > 0:
# await clb.answer(text=locale("sub_media_duplicates", "message", locale=user_locale))
# await clb.message.reply_text(locale("sub_media_duplicates_list", "message", locale=user_locale).format("\n • ".join(upload[1])))
# else:
# if clb.data.endswith("_caption"):
# index = jsonLoad(configGet("index", "locations"))
# index["captions"][Path(media).name] = submission.caption
# jsonSave(index, configGet("index", "locations"))
# else:
# media = await app.download_media(submission, file_name=configGet("queue", "locations")+sep)
# if clb.data.endswith("_caption"):
# index = jsonLoad(configGet("index", "locations"))
# index["captions"][Path(media).name] = submission.caption
# jsonSave(index, configGet("index", "locations"))
# except:
# await clb.answer(text=locale("sub_media_unavail", "message", locale=user_locale), show_alert=True)
# return
@app.on_callback_query(filters.regex("sub_no_[\s\S]*"))
async def callback_query_no(app: PosterClient, clb: CallbackQuery):
@Client.on_callback_query(filters.regex("sub_no_[\s\S]*"))
async def callback_query_no(app: PyroClient, clb: CallbackQuery):
fullclb = str(clb.data).split("_")
user_locale = clb.from_user.language_code
db_entry = col_submitted.find_one_and_delete({"_id": ObjectId(fullclb[2])})
if db_entry["temp"]["uuid"] is not None:
if path.exists(
path.join(
configGet("data", "locations"), "submissions", db_entry["temp"]["uuid"]
)
):
rmtree(
path.join(
configGet("data", "locations"),
"submissions",
db_entry["temp"]["uuid"],
),
ignore_errors=True,
)
if (
db_entry["temp"]["uuid"] is not None
and Path(
f"{app.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}"
).exists()
):
rmtree(
Path(
f"{app.config['locations']['data']}/submissions/{db_entry['temp']['uuid']}"
),
ignore_errors=True,
)
try:
submission = await app.get_messages(
db_entry["user"], db_entry["telegram"]["msg_id"]
)
except:
except Exception as exp:
await clb.answer(
text=locale("sub_msg_unavail", "message", locale=user_locale),
text=app._("sub_msg_unavail", "message", locale=user_locale),
show_alert=True,
)
return
await submission.reply_text(
locale("sub_no", "message", locale=submission.from_user.language_code),
app._("sub_no", "message", locale=submission.from_user.language_code),
quote=True,
)
await clb.answer(
text=locale("sub_no", "callback", locale=user_locale).format(fullclb[2]),
text=app._("sub_no", "callback", locale=user_locale).format(fullclb[2]),
show_alert=True,
)
@@ -175,7 +163,7 @@ async def callback_query_no(app: PosterClient, clb: CallbackQuery):
[
[
InlineKeyboardButton(
text=str(locale("declined", "button", locale=user_locale)),
text=str(app._("declined", "button", locale=user_locale)),
callback_data="nothing",
)
],
@@ -185,7 +173,7 @@ async def callback_query_no(app: PosterClient, clb: CallbackQuery):
else [
[
InlineKeyboardButton(
text=str(locale("declined", "button", locale=user_locale)),
text=str(app._("declined", "button", locale=user_locale)),
callback_data="nothing",
)
]
@@ -194,26 +182,28 @@ async def callback_query_no(app: PosterClient, clb: CallbackQuery):
await clb.message.edit_reply_markup(
reply_markup=InlineKeyboardMarkup(edited_markup)
)
logWrite(
locale(
logger.info(
app._(
"submission_rejected",
"console",
locale=configGet("locale_log").format(fullclb[2]),
),
debug=True,
locale=app.config["locale_log"],
).format(fullclb[2]),
)
@app.on_callback_query(filters.regex("sub_block_[\s\S]*"))
async def callback_query_block(app: PosterClient, clb: CallbackQuery):
@Client.on_callback_query(filters.regex("sub_block_[\s\S]*"))
async def callback_query_block(app: PyroClient, clb: CallbackQuery):
fullclb = str(clb.data).split("_")
user_locale = clb.from_user.language_code
await app.send_message(
int(fullclb[2]), locale("sub_blocked", "message", locale=configGet("locale"))
int(fullclb[2]),
app._("sub_blocked", "message"),
)
PosterUser(int(fullclb[2])).block()
await clb.answer(
text=locale("sub_block", "callback", locale=user_locale).format(fullclb[2]),
text=app._("sub_block", "callback", locale=user_locale).format(fullclb[2]),
show_alert=True,
)
@@ -221,7 +211,7 @@ async def callback_query_block(app: PosterClient, clb: CallbackQuery):
clb.message.reply_markup.inline_keyboard[0],
[
InlineKeyboardButton(
text=str(locale("sub_unblock", "button", locale=user_locale)),
text=str(app._("sub_unblock", "button", locale=user_locale)),
callback_data=f"sub_unblock_{fullclb[2]}",
)
],
@@ -229,26 +219,26 @@ async def callback_query_block(app: PosterClient, clb: CallbackQuery):
await clb.message.edit_reply_markup(
reply_markup=InlineKeyboardMarkup(edited_markup)
)
logWrite(
locale(
logger.info(
app._(
"user_blocked",
"console",
locale=configGet("locale_log").format(fullclb[2]),
),
debug=True,
locale=app.config["locale_log"],
).format(fullclb[2]),
)
@app.on_callback_query(filters.regex("sub_unblock_[\s\S]*"))
async def callback_query_unblock(app: PosterClient, clb: CallbackQuery):
@Client.on_callback_query(filters.regex("sub_unblock_[\s\S]*"))
async def callback_query_unblock(app: PyroClient, clb: CallbackQuery):
fullclb = str(clb.data).split("_")
user_locale = clb.from_user.language_code
await app.send_message(
int(fullclb[2]), locale("sub_unblocked", "message", locale=configGet("locale"))
)
await app.send_message(int(fullclb[2]), app._("sub_unblocked", "message"))
PosterUser(int(fullclb[2])).unblock()
await clb.answer(
text=locale("sub_unblock", "callback", locale=user_locale).format(fullclb[2]),
text=app._("sub_unblock", "callback", locale=user_locale).format(fullclb[2]),
show_alert=True,
)
@@ -256,7 +246,7 @@ async def callback_query_unblock(app: PosterClient, clb: CallbackQuery):
clb.message.reply_markup.inline_keyboard[0],
[
InlineKeyboardButton(
text=str(locale("sub_block", "button", locale=user_locale)),
text=str(app._("sub_block", "button", locale=user_locale)),
callback_data=f"sub_block_{fullclb[2]}",
)
],
@@ -264,11 +254,10 @@ async def callback_query_unblock(app: PosterClient, clb: CallbackQuery):
await clb.message.edit_reply_markup(
reply_markup=InlineKeyboardMarkup(edited_markup)
)
logWrite(
locale(
logger.info(
app._(
"user_unblocked",
"console",
locale=configGet("locale_log").format(fullclb[2]),
),
debug=True,
locale=app.config["locale_log"],
).format(fullclb[2]),
)

View File

@@ -1,45 +1,45 @@
from os import getpid, makedirs, path
from os import makedirs
from pathlib import Path
from time import time
from libbot import json_write
from pyrogram import filters
from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
from pyrogram.client import Client
from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
from classes.poster_client import PosterClient
from modules.app import app, users_with_context
from modules.logger import logWrite
from modules.scheduler import scheduler
from modules.utils import configGet, jsonSave, locale
from classes.pyroclient import PyroClient
from modules.utils import USERS_WITH_CONTEXT
@app.on_message(~filters.scheduled & filters.command(["shutdown"], prefixes=["", "/"]))
async def cmd_kill(app: PosterClient, msg: Message):
if msg.from_user.id in app.admins:
global users_with_context
if len(users_with_context) > 0:
await msg.reply_text(
f"There're {len(users_with_context)} unfinished users' contexts. If you turn off the bot, those will be lost. Please confirm shutdown using a button below.",
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
"Confirm shutdown", callback_data="shutdown"
)
]
]
),
)
return
pid = getpid()
logWrite(f"Shutting down bot with pid {pid}")
@Client.on_message(
~filters.scheduled & filters.command(["shutdown"], prefixes=["", "/"])
)
async def cmd_kill(app: PyroClient, msg: Message):
if msg.from_user.id not in app.admins:
return
if len(USERS_WITH_CONTEXT) > 0:
await msg.reply_text(
locale("shutdown", "message", locale=msg.from_user.language_code).format(
pid
app._("shutdown_confirm", "message").format(len(USERS_WITH_CONTEXT)),
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
app._(
"shutdown", "button", locale=msg.from_user.language_code
),
callback_data="shutdown",
)
]
]
),
)
scheduler.shutdown()
makedirs(configGet("cache", "locations"), exist_ok=True)
jsonSave(
{"timestamp": time()},
path.join(configGet("cache", "locations"), "shutdown_time"),
)
exit()
return
makedirs(app.config["locations"]["cache"], exist_ok=True)
await json_write(
{"timestamp": time()},
Path(f"{app.config['locations']['cache']}/shutdown_time"),
)
exit()

View File

@@ -1,23 +1,24 @@
from pyrogram import filters
from pyrogram.client import Client
from pyrogram.types import Message
from modules.app import app
from modules.utils import locale
from classes.pyroclient import PyroClient
from classes.user import PosterUser
from classes.poster_client import PosterClient
@app.on_message(~filters.scheduled & filters.command(["start"], prefixes="/"))
async def cmd_start(app: PosterClient, msg: Message):
if PosterUser(msg.from_user.id).is_blocked() is False:
await msg.reply_text(
locale("start", "message", locale=msg.from_user.language_code)
)
@Client.on_message(~filters.scheduled & filters.command(["start"], prefixes="/"))
async def cmd_start(app: PyroClient, msg: Message):
if PosterUser(msg.from_user.id).is_blocked():
return
await msg.reply_text(app._("start", "message", locale=msg.from_user.language_code))
@app.on_message(~filters.scheduled & filters.command(["rules", "help"], prefixes="/"))
async def cmd_rules(app: PosterClient, msg: Message):
if PosterUser(msg.from_user.id).is_blocked() is False:
await msg.reply_text(
locale("rules", "message", locale=msg.from_user.language_code)
)
@Client.on_message(
~filters.scheduled & filters.command(["rules", "help"], prefixes="/")
)
async def cmd_rules(app: PyroClient, msg: Message):
if PosterUser(msg.from_user.id).is_blocked():
return
await msg.reply_text(app._("rules", "message", locale=msg.from_user.language_code))

View File

@@ -1,214 +1,303 @@
import asyncio
import logging
from glob import iglob
from io import BytesIO
from os import getcwd, makedirs, path, remove
from pathlib import Path
from shutil import disk_usage, rmtree
from traceback import format_exc
from uuid import uuid4
from zipfile import ZipFile
from convopyro import listen_message
from photosapi_client.errors import UnexpectedStatus
from pyrogram import filters
from pyrogram.client import Client
from pyrogram.types import Message
from ujson import loads
from classes.poster_client import PosterClient
from modules.api_client import remove_pic, upload_pic
from modules.app import app, users_with_context
from modules.logger import logWrite
from modules.utils import configGet, extract_and_save, locale
from classes.pyroclient import PyroClient
from modules.api_client import (
BodyPhotoUpload,
File,
client,
photo_delete,
photo_upload,
)
from modules.utils import USERS_WITH_CONTEXT, extract_and_save
logger = logging.getLogger(__name__)
@app.on_message(~filters.scheduled & filters.command(["import"], prefixes=["", "/"]))
async def cmd_import(app: PosterClient, msg: Message):
if msg.from_user.id in app.admins:
global users_with_context
if msg.from_user.id not in users_with_context:
users_with_context.append(msg.from_user.id)
else:
return
@Client.on_message(~filters.scheduled & filters.command(["import"], prefixes=["", "/"]))
async def cmd_import(app: PyroClient, msg: Message):
if msg.from_user.id not in app.admins:
return
global USERS_WITH_CONTEXT
if msg.from_user.id not in USERS_WITH_CONTEXT:
USERS_WITH_CONTEXT.append(msg.from_user.id)
else:
return
await msg.reply_text(
app._("import_request", "message", locale=msg.from_user.language_code)
)
answer = await listen_message(app, msg.chat.id, timeout=600)
USERS_WITH_CONTEXT.remove(msg.from_user.id)
if answer is None:
await msg.reply_text(
locale("import_request", "message", locale=msg.from_user.language_code)
)
answer = await listen_message(app, msg.chat.id, timeout=600)
users_with_context.remove(msg.from_user.id)
if answer is None:
await msg.reply_text(
locale("import_ignored", "message", locale=msg.from_user.language_code),
quote=True,
)
return
if answer.text == "/cancel":
await answer.reply_text(
locale("import_abort", "message", locale=msg.from_user.language_code)
)
return
if answer.document is None:
await answer.reply_text(
locale(
"import_invalid_media",
"message",
locale=msg.from_user.language_code,
),
quote=True,
)
return
if answer.document.mime_type != "application/zip":
await answer.reply_text(
locale(
"import_invalid_mime", "message", locale=msg.from_user.language_code
),
quote=True,
)
return
if disk_usage(getcwd())[2] < (answer.document.file_size) * 3:
await msg.reply_text(
locale(
"import_too_big", "message", locale=msg.from_user.language_code
).format(
answer.document.file_size // (2**30),
disk_usage(getcwd())[2] // (2**30),
)
)
return
tmp_dir = str(uuid4())
logWrite(
f"Importing '{answer.document.file_name}' file {answer.document.file_size} bytes big (TMP ID {tmp_dir})"
)
makedirs(path.join(configGet("tmp", "locations"), tmp_dir), exist_ok=True)
tmp_path = path.join(configGet("tmp", "locations"), answer.document.file_id)
downloading = await answer.reply_text(
locale("import_downloading", "message", locale=msg.from_user.language_code),
quote=True,
)
await app.download_media(answer, file_name=tmp_path)
await downloading.edit(
locale("import_unpacking", "message", locale=msg.from_user.language_code)
)
try:
with ZipFile(tmp_path, "r") as handle:
tasks = [
extract_and_save(
handle, name, path.join(configGet("tmp", "locations"), tmp_dir)
)
for name in handle.namelist()
]
_ = await asyncio.gather(*tasks)
except Exception as exp:
logWrite(
f"Could not import '{answer.document.file_name}' due to {exp}: {format_exc}"
)
await answer.reply_text(
locale(
"import_unpack_error", "message", locale=msg.from_user.language_code
).format(exp, format_exc())
)
return
logWrite(f"Downloaded '{answer.document.file_name}' - awaiting upload")
await downloading.edit(
locale("import_uploading", "message", locale=msg.from_user.language_code)
)
remove(tmp_path)
for filename in iglob(
path.join(configGet("tmp", "locations"), tmp_dir) + "**/**", recursive=True
):
if not path.isfile(filename):
continue
# upload filename
uploaded = await upload_pic(filename)
if uploaded[0] is False:
logWrite(
f"Could not upload '{filename}' from '{path.join(configGet('tmp', 'locations'), tmp_dir)}'. Duplicates: {str(uploaded[1])}",
debug=True,
)
if len(uploaded[1]) > 0:
await msg.reply_text(
locale(
"import_upload_error_duplicate",
"message",
locale=msg.from_user.language_code,
).format(path.basename(filename)),
disable_notification=True,
)
else:
await msg.reply_text(
locale(
"import_upload_error_other",
"message",
locale=msg.from_user.language_code,
).format(path.basename(filename)),
disable_notification=True,
)
else:
logWrite(
f"Uploaded '{filename}' from '{path.join(configGet('tmp', 'locations'), tmp_dir)}' and got ID {uploaded[2]}",
debug=True,
)
await downloading.delete()
logWrite(
f"Removing '{path.join(configGet('tmp', 'locations'), tmp_dir)}' after uploading",
debug=True,
)
rmtree(path.join(configGet("tmp", "locations"), tmp_dir), ignore_errors=True)
await answer.reply_text(
locale("import_finished", "message", locale=msg.from_user.language_code),
app._("import_ignored", "message", locale=msg.from_user.language_code),
quote=True,
)
return
@app.on_message(~filters.scheduled & filters.command(["export"], prefixes=["", "/"]))
async def cmd_export(app: PosterClient, msg: Message):
if msg.from_user.id in app.admins:
pass
@app.on_message(~filters.scheduled & filters.command(["remove"], prefixes=["", "/"]))
async def cmd_remove(app: PosterClient, msg: Message):
if msg.from_user.id in app.admins:
global users_with_context
if msg.from_user.id not in users_with_context:
users_with_context.append(msg.from_user.id)
else:
return
await msg.reply_text(
locale("remove_request", "message", locale=msg.from_user.language_code)
if answer.text == "/cancel":
await answer.reply_text(
app._("import_abort", "message", locale=msg.from_user.language_code)
)
answer = await listen_message(app, msg.chat.id, timeout=600)
users_with_context.remove(msg.from_user.id)
if answer is None:
return
if answer.document is None:
await answer.reply_text(
app._(
"import_invalid_media",
"message",
locale=msg.from_user.language_code,
),
quote=True,
)
return
if answer.document.mime_type != "application/zip":
await answer.reply_text(
app._("import_invalid_mime", "message", locale=msg.from_user.language_code),
quote=True,
)
return
if disk_usage(getcwd())[2] < (answer.document.file_size) * 3:
await msg.reply_text(
app._(
"import_too_big", "message", locale=msg.from_user.language_code
).format(
answer.document.file_size // (2**30),
disk_usage(getcwd())[2] // (2**30),
)
)
return
tmp_dir = str(uuid4())
logging.info(
"Importing '%s' file %s bytes big (TMP ID %s)",
answer.document.file_name,
answer.document.file_size,
tmp_dir,
)
makedirs(Path(f"{app.config['locations']['tmp']}/{tmp_dir}"), exist_ok=True)
tmp_path = Path(f"{app.config['locations']['tmp']}/{answer.document.file_id}")
downloading = await answer.reply_text(
app._("import_downloading", "message", locale=msg.from_user.language_code),
quote=True,
)
await app.download_media(answer, file_name=str(tmp_path))
await downloading.edit(
app._("import_unpacking", "message", locale=msg.from_user.language_code)
)
try:
with ZipFile(tmp_path, "r") as handle:
tasks = [
extract_and_save(
handle, name, Path(f"{app.config['locations']['tmp']}/{tmp_dir}")
)
for name in handle.namelist()
]
_ = await asyncio.gather(*tasks)
except Exception as exp:
logger.error(
"Could not import '%s' due to %s: %s",
answer.document.file_name,
exp,
format_exc(),
)
await answer.reply_text(
app._(
"import_unpack_error", "message", locale=msg.from_user.language_code
).format(exp, format_exc())
)
return
logger.info("Downloaded '%s' - awaiting upload", answer.document.file_name)
await downloading.edit(
app._("import_uploading", "message", locale=msg.from_user.language_code)
)
remove(tmp_path)
for filename in iglob(
str(Path(f"{app.config['locations']['tmp']}/{tmp_dir}")) + "**/**",
recursive=True,
):
if not path.isfile(filename):
continue
with open(str(filename), "rb") as fh:
photo_bytes = BytesIO(fh.read())
try:
uploaded = await photo_upload(
app.config["posting"]["api"]["album"],
client=client,
multipart_data=BodyPhotoUpload(
File(photo_bytes, Path(filename).name, "image/jpeg")
),
ignore_duplicates=app.config["submission"]["allow_duplicates"],
compress=False,
caption="queue",
)
except UnexpectedStatus as exp:
logger.error(
"Could not upload '%s' from '%s': %s",
filename,
Path(f"{app.config['locations']['tmp']}/{tmp_dir}"),
exp,
)
await msg.reply_text(
locale("remove_ignored", "message", locale=msg.from_user.language_code),
quote=True,
app._(
"import_upload_error_other",
"message",
locale=msg.from_user.language_code,
).format(path.basename(filename)),
disable_notification=True,
)
return
if answer.text == "/cancel":
await answer.reply_text(
locale("remove_abort", "message", locale=msg.from_user.language_code)
)
return
response = await remove_pic(answer.text)
if response:
logWrite(
f"Removed '{answer.text}' by request of user {answer.from_user.id}"
)
await answer.reply_text(
locale(
"remove_success", "message", locale=msg.from_user.language_code
).format(answer.text)
continue
uploaded_dict = loads(uploaded.content.decode("utf-8"))
if "duplicates" in uploaded_dict:
logger.warning(
"Could not upload '%s' from '%s'. Duplicates: %s",
filename,
Path(f"{app.config['locations']['tmp']}/{tmp_dir}"),
str(uploaded_dict["duplicates"]),
)
if len(uploaded_dict["duplicates"]) > 0:
await msg.reply_text(
app._(
"import_upload_error_duplicate",
"message",
locale=msg.from_user.language_code,
).format(path.basename(filename)),
disable_notification=True,
)
else:
await msg.reply_text(
app._(
"import_upload_error_other",
"message",
locale=msg.from_user.language_code,
).format(path.basename(filename)),
disable_notification=True,
)
else:
logWrite(
f"Could not remove '{answer.text}' by request of user {answer.from_user.id}"
)
await answer.reply_text(
locale(
"remove_failure", "message", locale=msg.from_user.language_code
).format(answer.text)
logger.info(
"Uploaded '%s' from '%s' and got ID %s",
filename,
Path(f"{app.config['locations']['tmp']}/{tmp_dir}"),
uploaded.parsed.id,
)
await downloading.delete()
@app.on_message(~filters.scheduled & filters.command(["purge"], prefixes=["", "/"]))
async def cmd_purge(app: PosterClient, msg: Message):
if msg.from_user.id in app.admins:
pass
logger.info(
"Removing '%s' after uploading",
Path(f"{app.config['locations']['tmp']}/{tmp_dir}"),
)
rmtree(Path(f"{app.config['locations']['tmp']}/{tmp_dir}"), ignore_errors=True)
await answer.reply_text(
app._("import_finished", "message", locale=msg.from_user.language_code),
quote=True,
)
return
@Client.on_message(~filters.scheduled & filters.command(["export"], prefixes=["", "/"]))
async def cmd_export(app: PyroClient, msg: Message):
if msg.from_user.id not in app.admins:
return
@Client.on_message(~filters.scheduled & filters.command(["remove"], prefixes=["", "/"]))
async def cmd_remove(app: PyroClient, msg: Message):
if msg.from_user.id not in app.admins:
return
global USERS_WITH_CONTEXT
if msg.from_user.id not in USERS_WITH_CONTEXT:
USERS_WITH_CONTEXT.append(msg.from_user.id)
else:
return
await msg.reply_text(
app._("remove_request", "message", locale=msg.from_user.language_code)
)
answer = await listen_message(app, msg.chat.id, timeout=600)
USERS_WITH_CONTEXT.remove(msg.from_user.id)
if answer is None:
await msg.reply_text(
app._("remove_ignored", "message", locale=msg.from_user.language_code),
quote=True,
)
return
if answer.text == "/cancel":
await answer.reply_text(
app._("remove_abort", "message", locale=msg.from_user.language_code)
)
return
response = await photo_delete(id=answer.text, client=client)
if response:
logger.info(
"Removed '%s' by request of user %s", answer.text, answer.from_user.id
)
await answer.reply_text(
app._(
"remove_success", "message", locale=msg.from_user.language_code
).format(answer.text)
)
else:
logger.warning(
"Could not remove '%s' by request of user %s",
answer.text,
answer.from_user.id,
)
await answer.reply_text(
app._(
"remove_failure", "message", locale=msg.from_user.language_code
).format(answer.text)
)
@Client.on_message(~filters.scheduled & filters.command(["purge"], prefixes=["", "/"]))
async def cmd_purge(app: PyroClient, msg: Message):
if msg.from_user.id not in app.admins:
return

View File

@@ -1,32 +1,37 @@
import logging
from datetime import datetime
from os import makedirs, path, sep
from pathlib import Path
from traceback import format_exc
from uuid import uuid4
from pyrogram import filters
from pyrogram.client import Client
from pyrogram.enums.chat_action import ChatAction
from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
from classes.enums.submission_types import SubmissionType
from classes.exceptions import SubmissionDuplicatesError
from classes.poster_client import PosterClient
from classes.exceptions import SubmissionDuplicatesError, SubmissionUnsupportedError
from classes.pyroclient import PyroClient
from classes.user import PosterUser
from modules.app import app, users_with_context
from modules.database import col_banned, col_submitted
from modules.logger import logWrite
from modules.utils import configGet, locale
from modules.utils import USERS_WITH_CONTEXT
logger = logging.getLogger(__name__)
@app.on_message(
@Client.on_message(
~filters.scheduled & filters.private & filters.photo
| filters.video
| filters.animation
| filters.document
)
async def get_submission(app: PosterClient, msg: Message):
global users_with_context
if msg.from_user.id in users_with_context:
async def get_submission(app: PyroClient, msg: Message):
global USERS_WITH_CONTEXT
if msg.from_user.id in USERS_WITH_CONTEXT:
return
try:
if col_banned.find_one({"user": msg.from_user.id}) is not None:
return
@@ -39,34 +44,37 @@ async def get_submission(app: PosterClient, msg: Message):
if PosterUser(msg.from_user.id).is_limited():
await msg.reply_text(
locale("sub_cooldown", "message", locale=user_locale).format(
str(configGet("timeout", "submission"))
app._("sub_cooldown", "message", locale=user_locale).format(
str(app.config["submission"]["timeout"])
)
)
return
if msg.document is not None:
logWrite(
f"User {msg.from_user.id} is trying to submit a file of type '{msg.document.mime_type}' with name '{msg.document.file_name}' and size of {msg.document.file_size / 1024 / 1024} MB",
debug=True,
logger.info(
"User %s is trying to submit a file of type '%s' with name '%s' and size of %s MB",
msg.from_user.id,
msg.document.mime_type,
msg.document.file_name,
msg.document.file_size / 1024 / 1024,
)
if msg.document.mime_type not in configGet("mime_types", "submission"):
if msg.document.mime_type not in app.config["submission"]["mime_types"]:
await msg.reply_text(
locale("mime_not_allowed", "message", locale=user_locale).format(
", ".join(configGet("mime_types", "submission"))
app._("mime_not_allowed", "message", locale=user_locale).format(
", ".join(app.config["submission"]["mime_types"])
),
quote=True,
)
return
if msg.document.file_size > configGet("file_size", "submission"):
if msg.document.file_size > app.config["submission"]["file_size"]:
await msg.reply_text(
locale("document_too_large", "message", locale=user_locale).format(
str(configGet("file_size", "submission") / 1024 / 1024)
app._("document_too_large", "message", locale=user_locale).format(
str(app.config["submission"]["file_size"] / 1024 / 1024)
),
quote=True,
)
return
if msg.document.file_size > configGet("tmp_size", "submission"):
if msg.document.file_size > app.config["submission"]["tmp_size"]:
save_tmp = False
contents = (
msg.document.file_id,
@@ -74,36 +82,40 @@ async def get_submission(app: PosterClient, msg: Message):
) # , msg.document.file_name
if msg.video is not None:
logWrite(
f"User {msg.from_user.id} is trying to submit a video with name '{msg.video.file_name}' and size of {msg.video.file_size / 1024 / 1024} MB",
debug=True,
logger.info(
"User %s is trying to submit a video with name '%s' and size of %s MB",
msg.from_user.id,
msg.video.file_name,
msg.video.file_size / 1024 / 1024,
)
if msg.video.file_size > configGet("file_size", "submission"):
if msg.video.file_size > app.config["submission"]["file_size"]:
await msg.reply_text(
locale("document_too_large", "message", locale=user_locale).format(
str(configGet("file_size", "submission") / 1024 / 1024)
app._("document_too_large", "message", locale=user_locale).format(
str(app.config["submission"]["file_size"] / 1024 / 1024)
),
quote=True,
)
return
if msg.video.file_size > configGet("tmp_size", "submission"):
if msg.video.file_size > app.config["submission"]["tmp_size"]:
save_tmp = False
contents = msg.video.file_id, SubmissionType.VIDEO # , msg.video.file_name
if msg.animation is not None:
logWrite(
f"User {msg.from_user.id} is trying to submit an animation with name '{msg.animation.file_name}' and size of {msg.animation.file_size / 1024 / 1024} MB",
debug=True,
logger.info(
"User %s is trying to submit an animation with name '%s' and size of %s MB",
msg.from_user.id,
msg.animation.file_name,
msg.animation.file_size / 1024 / 1024,
)
if msg.animation.file_size > configGet("file_size", "submission"):
if msg.animation.file_size > app.config["submission"]["file_size"]:
await msg.reply_text(
locale("document_too_large", "message", locale=user_locale).format(
str(configGet("file_size", "submission") / 1024 / 1024)
app._("document_too_large", "message", locale=user_locale).format(
str(app.config["submission"]["file_size"] / 1024 / 1024)
),
quote=True,
)
return
if msg.animation.file_size > configGet("tmp_size", "submission"):
if msg.animation.file_size > app.config["submission"]["tmp_size"]:
save_tmp = False
contents = (
msg.animation.file_id,
@@ -111,26 +123,31 @@ async def get_submission(app: PosterClient, msg: Message):
) # , msg.animation.file_name
if msg.photo is not None:
logWrite(
f"User {msg.from_user.id} is trying to submit a photo with ID '{msg.photo.file_id}' and size of {msg.photo.file_size / 1024 / 1024} MB",
debug=True,
logger.info(
"User %s is trying to submit a photo with ID '%s' and size of %s MB",
msg.from_user.id,
msg.photo.file_id,
msg.photo.file_size / 1024 / 1024,
)
contents = msg.photo.file_id, SubmissionType.PHOTO # , "please_generate"
if save_tmp is not None:
if contents is None:
return
if contents is None:
return
if save_tmp is not None:
tmp_id = str(uuid4())
# filename = tmp_id if contents[1] == "please_generate" else contents[1]
makedirs(
path.join(configGet("data", "locations"), "submissions", tmp_id),
Path(f"{app.config['locations']['data']}/submissions/{tmp_id}"),
exist_ok=True,
)
downloaded = await app.download_media(
msg,
path.join(configGet("data", "locations"), "submissions", tmp_id) + sep,
str(Path(f"{app.config['locations']['data']}/submissions/{tmp_id}"))
+ sep,
)
inserted = col_submitted.insert_one(
{
"user": msg.from_user.id,
@@ -144,9 +161,6 @@ async def get_submission(app: PosterClient, msg: Message):
)
else:
if contents is None:
return
inserted = col_submitted.insert_one(
{
"user": msg.from_user.id,
@@ -162,7 +176,7 @@ async def get_submission(app: PosterClient, msg: Message):
buttons = [
[
InlineKeyboardButton(
text=locale("sub_yes", "button", locale=configGet("locale")),
text=app._("sub_yes", "button"),
callback_data=f"sub_yes_{str(inserted.inserted_id)}",
)
]
@@ -172,28 +186,20 @@ async def get_submission(app: PosterClient, msg: Message):
caption = str(msg.caption)
buttons[0].append(
InlineKeyboardButton(
text=locale(
"sub_yes_caption", "button", locale=configGet("locale")
),
text=app._("sub_yes_caption", "button"),
callback_data=f"sub_yes_{str(inserted.inserted_id)}_caption",
)
)
buttons[0].append(
InlineKeyboardButton(
text=locale("sub_no", "button", locale=configGet("locale")),
callback_data=f"sub_no_{str(inserted.inserted_id)}",
)
)
else:
caption = ""
buttons[0].append(
InlineKeyboardButton(
text=locale("sub_no", "button", locale=configGet("locale")),
callback_data=f"sub_no_{str(inserted.inserted_id)}",
)
)
caption += locale("sub_by", "message", locale=locale(configGet("locale")))
buttons[0].append(
InlineKeyboardButton(
text=app._("sub_no", "button"),
callback_data=f"sub_no_{str(inserted.inserted_id)}",
)
)
caption += app._("sub_by", "message")
if msg.from_user.first_name is not None:
caption += f" {msg.from_user.first_name}"
@@ -206,22 +212,30 @@ async def get_submission(app: PosterClient, msg: Message):
if (
msg.from_user.id in app.admins
and configGet("admins", "submission", "require_confirmation") is False
and app.config["submission"]["require_confirmation"]["admins"] is False
):
try:
submitted = await app.submit_photo(str(inserted.inserted_id))
submitted = await app.submit_media(str(inserted.inserted_id))
await msg.reply_text(
locale("sub_yes_auto", "message", locale=user_locale),
app._("sub_yes_auto", "message", locale=user_locale),
disable_notification=True,
quote=True,
)
if configGet("send_uploaded_id", "submission"):
if app.config["submission"]["send_uploaded_id"]:
caption += f"\n\nID: `{submitted[1]}`"
await msg.copy(app.owner, caption=caption, disable_notification=True)
return
except SubmissionUnsupportedError:
await msg.reply_text(
app._("mime_not_allowed", "message", locale=user_locale).format(
", ".join(app.config["submission"]["mime_types"]), quote=True
),
quote=True,
)
return
except SubmissionDuplicatesError as exp:
await msg.reply_text(
locale(
app._(
"sub_media_duplicates_list", "message", locale=user_locale
).format("\n".join(exp.duplicates)),
quote=True,
@@ -232,28 +246,35 @@ async def get_submission(app: PosterClient, msg: Message):
return
elif (
msg.from_user.id not in app.admins
and configGet("users", "submission", "require_confirmation") is False
and app.config["submission"]["require_confirmation"]["users"] is False
):
try:
submitted = await app.submit_photo(str(inserted.inserted_id))
await msg.reply_text(
locale("sub_yes_auto", "message", locale=user_locale),
app._("sub_yes_auto", "message", locale=user_locale),
disable_notification=True,
quote=True,
)
if configGet("send_uploaded_id", "submission"):
if app.config["submission"]["send_uploaded_id"]:
caption += f"\n\nID: `{submitted[1]}`"
await msg.copy(app.owner, caption=caption)
return
except SubmissionUnsupportedError:
await msg.reply_text(
app._("mime_not_allowed", "message", locale=user_locale).format(
", ".join(app.config["submission"]["mime_types"]), quote=True
)
)
return
except SubmissionDuplicatesError as exp:
await msg.reply_text(
locale("sub_dup", "message", locale=user_locale), quote=True
app._("sub_dup", "message", locale=user_locale), quote=True
)
return
except Exception as exp:
await app.send_message(
app.owner,
locale("sub_error_admin", "message").format(
app._("sub_error_admin", "message").format(
msg.from_user.id, format_exc()
),
)
@@ -264,7 +285,7 @@ async def get_submission(app: PosterClient, msg: Message):
buttons += [
[
InlineKeyboardButton(
text=locale("sub_block", "button", locale=configGet("locale")),
text=app._("sub_block", "button"),
callback_data=f"sub_block_{msg.from_user.id}",
)
]
@@ -274,7 +295,7 @@ async def get_submission(app: PosterClient, msg: Message):
if msg.from_user.id != app.owner:
await msg.reply_text(
locale("sub_sent", "message", locale=user_locale),
app._("sub_sent", "message", locale=user_locale),
disable_notification=True,
quote=True,
)
@@ -284,4 +305,4 @@ async def get_submission(app: PosterClient, msg: Message):
)
except AttributeError:
logWrite(f"from_user in function get_submission does not seem to contain id")
logger.error("'from_user' does not seem to contain 'id'")

View File

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

266
poster.py
View File

@@ -1,266 +0,0 @@
from datetime import datetime
from os import getpid, path
from sys import exit
from time import time
from traceback import format_exc
from modules.api_client import authorize
from modules.cli import *
from modules.http_client import http_session
from modules.logger import logWrite
from modules.scheduler import scheduler
from modules.utils import configGet, jsonLoad, jsonSave, locale
# Import ===================================================================================================================================
try:
from dateutil.relativedelta import relativedelta
from pyrogram.errors import bad_request_400
from pyrogram.sync import idle
from modules.app import app
except ModuleNotFoundError:
print(locale("deps_missing", "console", locale=configGet("locale")), flush=True)
exit()
# ===========================================================================================================================================
pid = getpid()
version = 0.1
# Work in progress
# def check_forwards(app):
# try:
# index = jsonLoad(configGet("index", "locations"))
# channel = app.get_chat(configGet("channel", "posting"))
# peer = app.resolve_peer(configGet("channel", "posting"))
# print(peer, flush=True)
# posts_list = [i for i in range(index["last_id"]-100,index["last_id"])]
# last_posts = app.get_messages(configGet("channel", "posting"), message_ids=posts_list)
# for post in last_posts:
# post_forwards = GetMessagePublicForwards(channel=peer, msg_id=post.id, offset_peer=peer, offset_rate=0, offset_id=0, limit=100)
# print(post_forwards, flush=True)
# for forward in post_forwards:
# print(forward, flush=True)
# except Exception as exp:
# logWrite("Could not get last posts forwards due to {0} with traceback {1}".format(str(exp), traceback.format_exc()), debug=True)
# if configGet("error", "reports"):
# app.send_message(configGet("admin"), traceback.format_exc())
# pass
# Work in progress
# @app.on_message(~ filters.scheduled & filters.command(["forwards"], prefixes="/"))
# def cmd_forwards(app, msg):
# check_forwards(app)
from plugins.callbacks.shutdown import *
# Imports ==================================================================================================================================
from plugins.commands.general import *
if configGet("submit", "mode"):
from plugins.callbacks.nothing import *
from plugins.callbacks.submission import *
from plugins.commands.mode_submit import *
from plugins.handlers.submission import *
if configGet("post", "mode"):
from plugins.commands.photos import *
# if configGet("api_based", "mode"):
# from modules.api_client import authorize
# ===========================================================================================================================================
# Work in progress
# Handle new forwards
# @app.on_raw_update()
# def fwd_got(app, update, users, chats):
# if isinstance(update, UpdateChannelMessageForwards):
# logWrite(f'Forward count increased to {update["forwards"]} on post {update["id"]} in channel {update["channel_id"]}')
# logWrite(str(users), debug=True)
# logWrite(str(chats), debug=True)
# # else:
# # logWrite(f"Got raw update of type {type(update)} with contents {update}", debug=True)
# async def main():
# await app.start()
# logWrite(locale("startup", "console", locale=configGet("locale")).format(str(pid)))
# if configGet("startup", "reports"):
# await app.send_message(configGet("admin"), locale("startup", "message", locale=configGet("locale")).format(str(pid)))
# if configGet("post", "mode"):
# scheduler.start()
# if configGet("api_based", "mode"):
# token = authorize()
# if len(get(f'{configGet("address", "posting", "api")}/albums?q={configGet("queue", "posting", "api", "albums")}', headers={"Authorization": f"Bearer {token}"}).json()["results"]) == 0:
# post(f'{configGet("address", "posting", "api")}/albums?name={configGet("queue", "posting", "api", "albums")}&title={configGet("queue", "posting", "api", "albums")}', headers={"Authorization": f"Bearer {token}"})
# await idle()
# await app.send_message(configGet("admin"), locale("shutdown", "message", locale=configGet("locale")).format(str(pid)))
# logWrite(locale("shutdown", "console", locale=configGet("locale")).format(str(pid)))
# killProc(pid)
# if __name__ == "__main__":
# if find_spec("uvloop") is not None:
# uvloop.install()
# asyncio.run(main())
async def main():
logWrite(locale("startup", "console").format(str(pid)))
await app.start()
if configGet("startup", "reports"):
try:
if path.exists(path.join(configGet("cache", "locations"), "shutdown_time")):
downtime = relativedelta(
datetime.now(),
datetime.fromtimestamp(
jsonLoad(
path.join(configGet("cache", "locations"), "shutdown_time")
)["timestamp"]
),
)
if downtime.days >= 1:
await app.send_message(
configGet("owner"),
locale(
"startup_downtime_days",
"message",
).format(pid, downtime.days),
)
elif downtime.hours >= 1:
await app.send_message(
configGet("owner"),
locale(
"startup_downtime_hours",
"message",
).format(pid, downtime.hours),
)
else:
await app.send_message(
configGet("owner"),
locale(
"startup_downtime_minutes",
"message",
locale=configGet("locale"),
).format(pid, downtime.minutes),
)
else:
await app.send_message(
configGet("owner"),
locale("startup", "message").format(pid),
)
except bad_request_400.PeerIdInvalid:
logWrite(
f"Could not send startup message to bot owner. Perhaps user has not started the bot yet."
)
if configGet("update", "reports"):
try:
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.", "")) > version:
logWrite(
f'New version {response[0]["tag_name"].replace("v.", "")} found (current {version})'
)
await app.send_message(
configGet("owner"),
locale(
"update_available",
"message",
).format(
response[0]["tag_name"],
response[0]["html_url"],
response[0]["body"],
),
)
else:
logWrite(f"No updates found, bot is up to date.")
except bad_request_400.PeerIdInvalid:
logWrite(
f"Could not send startup message to bot owner. Perhaps user has not started the bot yet."
)
except Exception as exp:
logWrite(f"Update check failed due to {exp}: {format_exc()}")
if configGet("post", "mode"):
scheduler.start()
try:
token = await authorize()
if (
len(
(
await (
await http_session.get(
f'{configGet("address", "posting", "api")}/albums?q={configGet("album", "posting", "api")}',
headers={"Authorization": f"Bearer {token}"},
)
).json()
)["results"]
)
== 0
):
logWrite("Media album does not exist on API server. Trying to create it...")
try:
await http_session.post(
f'{configGet("address", "posting", "api")}/albums?name={configGet("album", "posting", "api")}&title={configGet("album", "posting", "api")}',
headers={"Authorization": f"Bearer {token}"},
)
logWrite("Created media album on API server.")
except Exception as exp:
logWrite(
f"Could not create media album on API server due to {exp}: {format_exc()}"
)
except Exception as exp:
logWrite(f"Could not check if API album exists due to {exp}: {format_exc()}")
await idle()
try:
await app.send_message(
configGet("owner"),
locale("shutdown", "message").format(pid),
)
except bad_request_400.PeerIdInvalid:
logWrite(
f"Could not send shutdown message to bot owner. Perhaps user has not started the bot yet."
)
makedirs(configGet("cache", "locations"), exist_ok=True)
jsonSave(
{"timestamp": time()},
path.join(configGet("cache", "locations"), "shutdown_time"),
)
logWrite(locale("shutdown", "console").format(str(pid)))
scheduler.shutdown()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

View File

@@ -1,12 +1,17 @@
python_dateutil==2.8.2
apscheduler==3.10.1
pytimeparse==1.1.8
convopyro==0.5
pyrogram==2.0.104
aiofiles~=23.1.0
tgcrypto==1.2.5
aiohttp~=3.8.4
psutil==5.9.5
pymongo==4.3.3
pillow~=9.5.0
ujson==5.7.0
apscheduler~=3.10.1
black~=23.3.0
convopyro==0.5
pillow~=9.4.0
psutil~=5.9.4
pymongo~=4.3.3
pyrogram==2.0.106
python_dateutil==2.8.2
pytimeparse~=1.1.8
tgcrypto==1.2.5
ujson==5.8.0
uvloop==0.17.0
--extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple
libbot[speed,pyrogram]==1.0
photosapi_client==0.4.0