52 Commits

Author SHA1 Message Date
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
25 changed files with 214 additions and 1017 deletions

1
.gitignore vendored
View File

@@ -169,4 +169,3 @@ cython_debug/
cache cache
data data
logs logs
config.json

View File

@@ -1,20 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
],
"baseBranches": [
"dev"
],
"packageRules": [
{
"matchUpdateTypes": [
"minor",
"patch",
"pin",
"digest"
],
"automerge": true
}
]
}

View File

@@ -11,11 +11,7 @@ This bot is used for one and only task - post pictures from my personal archive.
## Dependencies ## Dependencies
* [Python 3.7+](https://www.python.org) (3.9+ recommended) For now bot requires [MongoDB](https://www.mongodb.com) and [PhotosAPI](https://git.end-play.xyz/profitroll/PhotosAPI) in order to function properly. Use [MongoDB's installation manual](https://www.mongodb.com/docs/manual/installation) and [Photos API's README](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md) to install install them.
* [MongoDB](https://www.mongodb.com)
* [PhotosAPI](https://git.end-play.xyz/profitroll/PhotosAPI)
Use [MongoDB's installation manual](https://www.mongodb.com/docs/manual/installation) and [Photos API's README](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md).
Please note that Photos API also requires MongoDB so it makes sense to install and configure Mongo first. Please note that Photos API also requires MongoDB so it makes sense to install and configure Mongo first.
@@ -30,58 +26,51 @@ To make this bot run at first you need to have a Python interpreter, Photos API,
> it in scripts you will use (`loop.sh`, `loop.bat`, `start.sh` and `start.bat`). > it in scripts you will use (`loop.sh`, `loop.bat`, `start.sh` and `start.bat`).
1. Install Mongo and Photos API: 1. Install Mongo and Photos API:
1. Install MongoDB by following [official installation manual](https://www.mongodb.com/docs/manual/installation) 1. Install MongoDB by following [official installation manual](https://www.mongodb.com/docs/manual/installation)
2. Install Photos API by following [Photos API's README](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md) 2. Install Photos API by following [Photos API's README](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md)
2. Download the bot: 2. Download the bot:
1. `git clone -b dev https://git.end-play.xyz/profitroll/TelegramSender.git` (if you want to use git)
2. `cd ./TelegramSender`
1. `git clone -b dev https://git.end-play.xyz/profitroll/TelegramPoster.git` (if you're using git) 3. Install project's dependencies:
2. `cd TelegramPoster`
3. Create virtual environment [Optional]:
1. Install virtualenv module: `pip install virtualenv`
2. Create venv: `python -m venv env`
3. Activate it using `source venv/bin/activate` on Linux, `venv\Scripts\activate.bat` in CMD or `venv\Scripts\Activate.ps1` in PowerShell.
4. Install project's dependencies:
`python -m pip install -r requirements.txt` `python -m pip install -r requirements.txt`
Without installing those - bot cannot work at all. Without installing those - bot cannot work at all
5. Configure "bot" and "owner" with your favorite text editor: 4. Install optional dependencies [Not required]:
`python -m pip install -r requirements-optional.txt`
1. Copy file `config_example.json` to `config.json` These are not required but can make the bot run a bit faster
2. Open `config.json` using your favorite text editor. For example `nano config.json`, but you can edit with vim, nano, on Windows it's Notepad or Notepad++. Whatever
3. Change `"owner"`, `"bot.api_id"`, `"bot.api_hash"` and `"bot.bot_token"` keys' values.
5. Configure your bot with a favorite text editor:
`nano config.json`
You can edit with vim, nano, on Windows it's Notepad or Notepad++. Whatever.
If you don't know where to find bot_token and your id - here you can find some hints: [get bot token](https://www.siteguarding.com/en/how-to-get-telegram-bot-api-token), [get your id](https://www.alphr.com/telegram-find-user-id), [get api_hash and api_id](https://core.telegram.org/api/obtaining_api_id). If you don't know where to find bot_token and your id - here you can find some hints: [get bot token](https://www.siteguarding.com/en/how-to-get-telegram-bot-api-token), [get your id](https://www.alphr.com/telegram-find-user-id), [get api_hash and api_id](https://core.telegram.org/api/obtaining_api_id).
6. Configure database and API: 6. Configure database and API:
1. Configure database: 1. Configure database:
1. Change database host and port in keys `"database.host"` and `"database.port"`. For default local installation those will be `127.0.0.1` and `27017` respectively 1. Change database host and port in keys `"database.host"` and `"database.port"`;
2. Change database name to the one you like in `"database.name"`. It will be automatically created on start 2. Change database name to the one you like in `"database.name"`;
3. If you've changed user and password to access the db, you should also change `"database.user"` and `"database.password"` keys, otherwise leave them `null` (default). 3. If you've changed user and password to access the db you should also change `"database.user"` and `"database.password"` keys, otherwise leave them `null` (default).
2. Configure Photos API: 2. Configure Photos API:
1. Change `"posting.api.address"` to the one your API servers uses 1. Create new user and album
2. Run your bot using `python poster.py --create-user --create-album` to configure its new user and album. You can also use manual user and album creation described [in the wiki](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Configuring-API). You can also change username, password and album in`"posting.api"` to the user and album you have if you already have Photos API album and user set up. In that case you don't need to create a new one. 1. Register new user using API's `POST /users` request (`/docs#/default/user_create_users_post`). Please make sure you're using a valid email address if you've configured you API to use email confirmation;
2. If you have email confirmation - activate your new user by using `PATCH /users/{user}/confirm` (`/docs#/default/user_confirm_users__user__confirm_patch`) or by using the link from the email;
3. Now create a new album for your bot using your user. Click "Authorize" on the docs page of your API, enter your login and password, select "select all" in scopes section and click "Authorize". Now you can execute requests as your user;
4. Go to `POST /albums` (`docs#/default/album_create_albums_post`) and create a friendly name for your album. No worries, only bot can see it so it doesn't matter what name you use;
2. Configure bot to use that album
1. Now you can configure your bot to use all of that. Change key `"posting.api.address"` to the http/https address accessible by the bot. Avoid using external address without a reason if API and the bot are on the same machine. If you want to see duplicates from the outside you can also change `"posting.api.address_external"` to the one accessible from the internet;
2. Configure API user/password and your album name using `"posting.api.username"`, `"posting.api.password"` and `"posting.api.album"` keys.
7. Add bot to the channel: 7. Add bot to the channel:
To use your bot of course you need to have a channel or group otherwise makes no sense to have such a bot. [Here](https://stackoverflow.com/a/33497769) you can find a quick guide how to add your bot to a channel. After that simply set `"posting.channel"` to your channel's ID. To use your bot of course you need to have a channel or group otherwise makes no sense to have such a bot. [Here](https://stackoverflow.com/a/33497769) you can find a quick guide how to add your bot to a channel. After that simply set `"posting.channel"` to your channel's ID.
8. Configure posting time: 8. Configure posting time:
To make your bot post random content you need to configure `"posting.time"` with a list of "DD:MM" formatted strings or use `"posting.interval"` formatted as "XdXhXmXs". To use interval instead of selected time set `"posting.use_interval"` to `true`. To make your bot post random content you need to configure `"posting.time"` with a list of "DD:MM" formatted strings or use `"posting.interval"` formatted as "XdXhXmXs". To use interval instead of selected time set `"posting.use_interval"` to `true`.
9. Good to go, run it! 9. Good to go, run it!
Make sure MongoDB and Photos API are running and use `python ./main.py` to start it.
Make sure MongoDB and Photos API are running and use `python poster.py` to start it.
Or you can also use `.\start.bat` on Windows and `bash ./start.sh` on Linux. Or you can also use `.\start.bat` on Windows and `bash ./start.sh` on Linux.
Additionally there are `loop.sh` and `loop.bat` available if you want your bot to start again after being stopped or after using `/shutdown` command. Additionally there are `loop.sh` and `loop.bat` available if you want your bot to start again after being stopped or after using `/reboot` command.
If you need any further instructions on how to configure your bot or you had any difficulties doing so - please use [wiki in this repository](https://git.end-play.xyz/profitroll/TelegramPoster/wiki) to get more detailed instructions. If you need any further instructions on how to configure your bot or you had any difficulties doing so - please use [wiki in this repository](https://git.end-play.xyz/profitroll/TelegramPoster/wiki) to get more detailed instructions.
@@ -89,17 +78,15 @@ If you need any further instructions on how to configure your bot or you had any
Of course bot also has them. You can perform some actions with them. Of course bot also has them. You can perform some actions with them.
* `--create-user` - create new API user. Requires config key `"posting.api.address"` to be set; * `--move-sent` - allows you to move all sent files from queue to sent directories
* `--create-album` - create new API album. Requires API address and user config (`"posting.api"`) to be complete. * `--cleanup` - purge files in both `queue` and `sent` folders if they're sent. Requires `--confirm` argument
* `--cleanup-index` - purge all sent entries from index. Requires `--confirm` argument
* `--norun` - allows you to execute above arguments without triggering the bot start itself
Examples: Examples:
* `python poster.py --create-user` * `python3 ./main.py --move-sent --norun`
* `python poster.py --create-user --create-album` * `python3 ./main.py --cleanup --confirm`
## Tips and improvements
* You may want to configure your bot to work as a systemd service instead. There's [a tutorial for that](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Configuring-Service) in the wiki.
## Localization ## Localization

View File

@@ -5,8 +5,6 @@
<a href="https://git.end-play.xyz/profitroll/TelegramPoster"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a> <a href="https://git.end-play.xyz/profitroll/TelegramPoster"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
</p> </p>
## ⚠️ Українська версія README dev гілки ще не готова! Користуйтесь англійською! ⚠️
Цей бот використовується для однієї-єдиної задачі - розміщувати фотографії з мого особистого архіву. Ось його код, тож Ви також можете захостити бота самостійно та розважитися з ним. Тільки не очікуйте, що він ідеальним. Не буде. Але гей, Ви завжди можете його доробити під себе ;) Цей бот використовується для однієї-єдиної задачі - розміщувати фотографії з мого особистого архіву. Ось його код, тож Ви також можете захостити бота самостійно та розважитися з ним. Тільки не очікуйте, що він ідеальним. Не буде. Але гей, Ви завжди можете його доробити під себе ;)
## Установка ## Установка

View File

@@ -20,39 +20,3 @@ class SubmissionDuplicatesError(Exception):
super().__init__( super().__init__(
f"Found duplicates of a photo '{file_path}': {self.duplicates}" f"Found duplicates of a photo '{file_path}': {self.duplicates}"
) )
class UserCreationError(Exception):
def __init__(self, code: int, data: str) -> None:
self.code = code
self.data = data
super().__init__(
f"Could not create a new user. API returned HTTP {self.code} with content: {self.data}"
)
class UserCreationDuplicateError(Exception):
def __init__(self, username: str) -> None:
self.username = username
super().__init__(f"User '{self.username} already exists.'")
class AlbumCreationError(Exception):
def __init__(self, code: int, data: str) -> None:
self.code = code
self.data = data
super().__init__(
f"Could not create a new album. API returned HTTP {self.code} with content: {self.data}"
)
class AlbumCreationDuplicateError(Exception):
def __init__(self, name: str) -> None:
self.name = name
super().__init__(f"Album '{self.name} already exists.'")
class AlbumCreationNameError(Exception):
def __init__(self, data: dict) -> None:
self.data = data
super().__init__(data["detail"])

View File

@@ -1,6 +1,6 @@
from os import path, remove, sep from os import path, remove, sep
from shutil import rmtree from shutil import rmtree
from typing import Tuple, Union from typing import Union
from pyrogram.client import Client from pyrogram.client import Client
from pyrogram.types import Message from pyrogram.types import Message
from classes.exceptions import SubmissionDuplicatesError, SubmissionUnavailableError from classes.exceptions import SubmissionDuplicatesError, SubmissionUnavailableError
@@ -18,9 +18,7 @@ class PosterClient(Client):
self.owner = configGet("owner") self.owner = configGet("owner")
self.admins = configGet("admins") + [configGet("owner")] self.admins = configGet("admins") + [configGet("owner")]
async def submit_photo( async def submit_photo(self, id: str) -> Union[Message, None]:
self, id: str
) -> Tuple[Union[Message, None], Union[str, None]]:
db_entry = col_submitted.find_one({"_id": ObjectId(id)}) db_entry = col_submitted.find_one({"_id": ObjectId(id)})
submission = None submission = None
@@ -89,7 +87,7 @@ class PosterClient(Client):
f"Could not delete '{filepath}' on submission accepted", debug=True f"Could not delete '{filepath}' on submission accepted", debug=True
) )
return submission, response[2] return submission
async def ban_user(self, id: int) -> None: async def ban_user(self, id: int) -> None:
pass pass

View File

@@ -23,7 +23,6 @@
"reports": { "reports": {
"sent": false, "sent": false,
"error": true, "error": true,
"update": true,
"startup": true, "startup": true,
"shutdown": true "shutdown": true
}, },
@@ -87,17 +86,14 @@
}, },
"caption": { "caption": {
"enabled": false, "enabled": false,
"link": null, "text": "sample text",
"text": [ "link": null
"sample text"
]
}, },
"submission": { "submission": {
"timeout": 30, "timeout": 30,
"file_size": 15728640, "file_size": 15728640,
"tmp_size": 15728640, "tmp_size": 15728640,
"allow_duplicates": false, "allow_duplicates": false,
"send_uploaded_id": false,
"require_confirmation": { "require_confirmation": {
"users": true, "users": true,
"admins": true "admins": true
@@ -117,7 +113,6 @@
"commands_admin": [ "commands_admin": [
"import", "import",
"export", "export",
"remove", "reboot"
"shutdown"
] ]
} }

1
docs/config_venv.md Normal file
View File

@@ -0,0 +1 @@
# Configuring virtual environment

View File

@@ -1 +0,0 @@
# Updating your bot

View File

@@ -9,16 +9,13 @@
"export": "Get .zip archive with all photos", "export": "Get .zip archive with all photos",
"remove": "Delete photo by its ID", "remove": "Delete photo by its ID",
"purge": "Completely purge bot's queue", "purge": "Completely purge bot's queue",
"shutdown": "Turn off the bot" "reboot": "Restart the bot"
}, },
"message": { "message": {
"start": "Hi and welcome!\n\nYou can submit your pictures and videos here. We'll review and add them, if we like them. Make sure you send your stuff one at a time and have chosen media that corresponds to our rules.\n\nYou can also write something to us in the description field. We'll send it with the submission itself, if needed.\n\nAlso, make sure you follow the /rules of submission, otherwise your submission will be declined. In case of spam/abuse you may even be blocked.\n\nHave fun and happy submitting!", "start": "Hi and welcome!\n\nYou can submit your pictures and videos here. We'll review and add them, if we like them. Make sure you send your stuff one at a time and have chosen media that corresponds to our rules.\n\nYou can also write something to us in the description field. We'll send it with the submission itself, if needed.\n\nAlso, make sure you follow the /rules of submission, otherwise your submission will be declined. In case of spam/abuse you may even be blocked.\n\nHave fun and happy submitting!",
"rules": "Photos submission rules:\n1. No porn, only erotics and aesthetics\n2. Nipples are semi-allowed, should be either veiled or barely visible\n3. Genitalia strictly prohibited, but labia prints on clothes or nice pubes/panties/butts - are fine\n4. Submitting russians is forbidden", "rules": "Photos submission rules:\n1. No porn, only erotics and aesthetics\n2. Nipples are semi-allowed, should be either veiled or barely visible\n3. Genitalia strictly prohibited, but labia prints on clothes or nice pubes/panties/butts - are fine\n4. Submitting russians is forbidden",
"shutdown": "Shutting down bot with pid `{0}`", "shutdown": "Shutting down bot with pid `{0}`",
"startup": "Starting with pid `{0}`", "startup": "Starting with pid `{0}`",
"startup_downtime_minutes": "Starting with pid `{0}` (was down for {1} m.)",
"startup_downtime_hours": "Starting with pid `{0}` (was down for {1} h.)",
"startup_downtime_days": "Starting with pid `{0}` (was down for {1} d.)",
"sub_yes": "✅ Submission approved and accepted", "sub_yes": "✅ Submission approved and accepted",
"sub_yes_auto": "✅ Submission automatically accepted", "sub_yes_auto": "✅ Submission automatically accepted",
"sub_no": "❌ Submission reviewed and declined", "sub_no": "❌ Submission reviewed and declined",
@@ -40,27 +37,9 @@
"post_low": "Low amount of content: `There are only {0} files left in the queue.`", "post_low": "Low amount of content: `There are only {0} files left in the queue.`",
"api_creds_invalid": "Could not authorize API access. Please check whether provided in config file are valid and update them if they're not.", "api_creds_invalid": "Could not authorize API access. Please check whether provided in config file are valid and update them if they're not.",
"sub_wip": "Post submission is now WIP. It will be available again in a few days. Thank you for your patience.", "sub_wip": "Post submission is now WIP. It will be available again in a few days. Thank you for your patience.",
"sub_duplicates_found": "__TO_BE_ADDED__",
"sub_error": "⚠️ Could not upload this image due to bot error. Admins are advised.", "sub_error": "⚠️ Could not upload this image due to bot error. Admins are advised.",
"sub_error_admin": "User {0} could not submit photo without additional confirmation due to:\n```\n{1}\n```", "sub_error_admin": "User {0} could not submit photo without additional confirmation due to:\n```\n{1}\n```"
"import_request": "Please send me a zip archive with your media to be imported. Use /cancel if you want to abort this operation.",
"import_ignored": "No response, aborting import.",
"import_abort": "Import aborted.",
"import_invalid_media": "File to import must be a zip archive. Aborting.",
"import_invalid_mime": "Provided file is not supported. Please send `application/zip`. Aborting.",
"import_too_big": "You archive is `{0} GiB` big, but system has only `{1} GiB` free. Unpacking may take even more space. Aborting.",
"import_downloading": "Downloading archive...",
"import_unpacking": "Unpacking archive...",
"import_unpack_error": "Could not unpack the archive\n\nException: {0}\n\nTraceback:\n```python\n{1}\n```",
"import_uploading": "Uploading archive contents...",
"import_upload_error_duplicate": "Could not upload `{0}` because there're duplicates on server.",
"import_upload_error_other": "Could not upload `{0}`. Probably disallowed filetype.",
"import_finished": "Import finished.",
"remove_request": "Please send me an ID to delete. You might have it from upload dialog. Use /cancel if you want to abort this operation.",
"remove_ignored": "No response, aborting removal.",
"remove_abort": "Removal aborted.",
"remove_success": "Removed media with ID `{0}`.",
"remove_failure": "Could not remove media with ID `{0}`. Check if provided ID is correct and if it is - you can also check bot's log for details.",
"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."
}, },
"button": { "button": {
"sub_yes": "✅ Accept", "sub_yes": "✅ Accept",
@@ -80,12 +59,13 @@
"sub_msg_unavail": "Submission message no longer exist", "sub_msg_unavail": "Submission message no longer exist",
"sub_media_unavail": "Could not download submission", "sub_media_unavail": "Could not download submission",
"sub_done": "You've already decided what to do with submission", "sub_done": "You've already decided what to do with submission",
"sub_upload_failed": "__TO_BE_ADDED__",
"sub_duplicates_found": "There're duplicates in bot's database", "sub_duplicates_found": "There're duplicates in bot's database",
"nothing": "🏁 This action is already finished" "nothing": "🏁 This action is already finished"
}, },
"console": { "console": {
"shutdown": "Shutting down bot with pid {0}", "shutdown": "Shutting down bot with pid {0}",
"startup": "Starting with pid {0}", "startup":"Starting with pid {0}",
"keyboard_interrupt": "\nShutting down...", "keyboard_interrupt": "\nShutting down...",
"exception_occured": "Exception {0} happened on task execution", "exception_occured": "Exception {0} happened on task execution",
"post_sent": "Sent {0} to {1} with caption {2} and silently {3}", "post_sent": "Sent {0} to {1} with caption {2} and silently {3}",
@@ -114,17 +94,6 @@
"cleanup_completed": "Performed cleanup of the sent files", "cleanup_completed": "Performed cleanup of the sent files",
"cleanup_unathorized": "Requested cleanup of sent files but not authorized. Please pass '--confirm' to perform that", "cleanup_unathorized": "Requested cleanup of sent files but not authorized. Please pass '--confirm' to perform that",
"cleanup_index_completed": "Performed cleanup of sent files index", "cleanup_index_completed": "Performed cleanup of sent files index",
"cleanup_index_unathorized": "Requested cleanup of sent files index but not authorized. Please pass '--confirm' to perform that", "cleanup_index_unathorized": "Requested cleanup of sent files index but not authorized. Please pass '--confirm' to perform that"
"random_pic_response": "Random pic response: {0}",
"random_pic_error_code": "Could not get photos from album {0}: HTTP {1}",
"random_pic_error_debug": "Could not get photos from '{0}/albums/{1}/photos?q=&page_size={2}&caption=queue' using token '{3}': HTTP {4}",
"find_pic_error": "Could not find image with name '{0}' and caption '{1}' due to: {2}",
"pic_upload_error": "Could not upload '{0}' to API: HTTP {1} with message '{2}'",
"api_creds_invalid": "Incorrect API credentials! Could not login into '{0}' using login '{1}': HTTP {2}",
"user_blocked": "User {0} has been blocked",
"user_unblocked": "User {0} has been unblocked",
"submission_accepted": "Submission with ID '{0}' accepted and uploaded with ID '{1}'",
"submission_rejected": "Submission with ID '{0}' rejected",
"submission_duplicate": "Submission with ID '{0}' could not be accepted because of the duplicates: {1}"
} }
} }

View File

@@ -9,16 +9,13 @@
"export": "Отримати .zip архів з усіма фотографіями", "export": "Отримати .zip архів з усіма фотографіями",
"remove": "Видалити фото за його ID", "remove": "Видалити фото за його ID",
"purge": "Повністю видалити всю чергу бота", "purge": "Повністю видалити всю чергу бота",
"shutdown": "Вимкнути бота" "reboot": "Перезапустити бота"
}, },
"message": { "message": {
"start": "Привіт і ласкаво просимо!\n\nТут можна пропонувати свої фотографії та відео. Ми переглянемо та додамо їх, якщо вони нам сподобаються. Переконайтеся, що ви надсилаєте свої матеріали по одному та вибираєте медіа, які відповідають нашим правилам.\n\nВи також можете написати нам щось у полі опису. За потреби ми надішлемо це разом із самим фото.\n\nКрім того, переконайтеся, що ви дотримуєтеся /rules (правил) подання, інакше вашу пропозицію буде відхилено. У разі спаму/зловживань вас можуть навіть заблокувати.\n\nГарного дня та щасливого надсилання!", "start": "Привіт і ласкаво просимо!\n\nТут можна пропонувати свої фотографії та відео. Ми переглянемо та додамо їх, якщо вони нам сподобаються. Переконайтеся, що ви надсилаєте свої матеріали по одному та вибираєте медіа, які відповідають нашим правилам.\n\nВи також можете написати нам щось у полі опису. За потреби ми надішлемо це разом із самим фото.\n\nКрім того, переконайтеся, що ви дотримуєтеся /rules (правил) подання, інакше вашу пропозицію буде відхилено. У разі спаму/зловживань вас можуть навіть заблокувати.\n\nГарного дня та щасливого надсилання!",
"rules": "Правила пропонування фото:\n1. Ніякого порно, тільки еротика та естетика\n2. Соски можна, але або завуальовані, або зовсім ледь помітні\n3. Геніталії суворо ні, а ось відбитки статевих губ на одязі або гарні лобочки/трусики/попки - без проблем\n4. Пропонувати русню заборонено", "rules": "Правила пропонування фото:\n1. Ніякого порно, тільки еротика та естетика\n2. Соски можна, але або завуальовані, або зовсім ледь помітні\n3. Геніталії суворо ні, а ось відбитки статевих губ на одязі або гарні лобочки/трусики/попки - без проблем\n4. Пропонувати русню заборонено",
"shutdown": "Вимкнення бота з підом `{0}`", "shutdown": "Вимкнення бота з підом `{0}`",
"startup": "Запуск бота з підом `{0}`", "startup": "Запуск бота з підом `{0}`",
"startup_downtime_minutes": "Запуск бота з підом `{0}` (лежав {1} хв.)",
"startup_downtime_hours": "Запуск бота з підом `{0}` (лежав {1} год.)",
"startup_downtime_days": "Запуск бота з підом `{0}` (лежав {1} дн.)",
"sub_yes": "✅ Подання схвалено та прийнято", "sub_yes": "✅ Подання схвалено та прийнято",
"sub_yes_auto": "✅ Подання автоматично прийнято", "sub_yes_auto": "✅ Подання автоматично прийнято",
"sub_no": "❌ Подання розглянуто та відхилено", "sub_no": "❌ Подання розглянуто та відхилено",
@@ -40,27 +37,9 @@
"post_low": "Мала кількість контенту: `Залишилось всього {0} файлів в черзі.`", "post_low": "Мала кількість контенту: `Залишилось всього {0} файлів в черзі.`",
"api_creds_invalid": "Не вдалося авторизувати запит до API. Будь ласка, перевірте чи дані авторизації в конфігураційному файлі вірні та оновіть їх, якщо це не так.", "api_creds_invalid": "Не вдалося авторизувати запит до API. Будь ласка, перевірте чи дані авторизації в конфігураційному файлі вірні та оновіть їх, якщо це не так.",
"sub_wip": "Подання постів зараз знаходиться у розробці. Він буде знову доступний через кілька днів. Дякуємо за ваше терпіння.", "sub_wip": "Подання постів зараз знаходиться у розробці. Він буде знову доступний через кілька днів. Дякуємо за ваше терпіння.",
"sub_duplicates_found": "__TO_BE_ADDED__",
"sub_error": "⚠️ Не вдалось завантажити фото через помилку бота. Адміністрацію повідомлено.", "sub_error": "⚠️ Не вдалось завантажити фото через помилку бота. Адміністрацію повідомлено.",
"sub_error_admin": "Користувач {0} не зміг надіслати фото без додаткової перевірки через помилку:\n```\n{1}\n```", "sub_error_admin": "Користувач {0} не зміг надіслати фото без додаткової перевірки через помилку:\n```\n{1}\n```"
"import_request": "Будь ласка, надішліть zip-архів з медіа для імпортування. Використовуйте /cancel, якщо ви хочете перервати цю операцію.",
"import_ignored": "Немає відповіді, перериваємо імпорт.",
"import_abort": "Імпорт перервано.",
"import_invalid_media": "Файл для імпорту має бути zip-архівом. Перериваємо.",
"import_invalid_mime": "Наданий файл не підтримується. Будь ласка, надішліть `application/zip`. Перервано.",
"import_too_big": "Ваш архів має розмір `{0} GiB`, але система має лише `{1} GiB` вільних. Розпакування може зайняти значно більше місця. Перервано.",
"import_downloading": "Завантажуємо архів...",
"import_unpacking": "Розпаковуємо архів...",
"import_unpack_error": "Не вдалося розпакувати архів\n\nПомилка: {0}\n\nTraceback:\n```python\n{1}\n```",
"import_uploading": "Завантажуємо вміст архіву...",
"import_upload_error_duplicate": "Не вдалося завантажити `{0}`, оскільки на сервері є дублікати.",
"import_upload_error_other": "Не вдалося завантажити `{0}`. Ймовірно, заборонений тип файлу.",
"import_finished": "Імпорт завершено.",
"remove_request": "Будь ласка, надішліть мені ID для видалення. Ви могли отримати його з діалогу завантаження. Використовуйте /cancel, якщо ви хочете перервати цю операцію.",
"remove_ignored": "Немає відповіді, перериваємо видалення.",
"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` у конфігурації."
}, },
"button": { "button": {
"sub_yes": "✅ Прийняти", "sub_yes": "✅ Прийняти",
@@ -80,6 +59,7 @@
"sub_msg_unavail": "Повідомлення більше не існує", "sub_msg_unavail": "Повідомлення більше не існує",
"sub_media_unavail": "Не вдалося завантажити подання", "sub_media_unavail": "Не вдалося завантажити подання",
"sub_done": "Ви вже обрали що зробити з цим поданням", "sub_done": "Ви вже обрали що зробити з цим поданням",
"sub_upload_failed": "__TO_BE_ADDED__",
"sub_duplicates_found": "Знайдено дублікати в базі даних бота", "sub_duplicates_found": "Знайдено дублікати в базі даних бота",
"nothing": "🏁 Цю дію вже було завершено" "nothing": "🏁 Цю дію вже було завершено"
}, },
@@ -114,17 +94,6 @@
"cleanup_completed": "Виконано очищення надісланих файлів", "cleanup_completed": "Виконано очищення надісланих файлів",
"cleanup_unathorized": "Надіслано запит на очищення надісланих файлів, але не авторизовано. Для цього надайте аргумент '--confirm'", "cleanup_unathorized": "Надіслано запит на очищення надісланих файлів, але не авторизовано. Для цього надайте аргумент '--confirm'",
"cleanup_index_completed": "Виконано очищення індексу надісланих файлів", "cleanup_index_completed": "Виконано очищення індексу надісланих файлів",
"cleanup_index_unathorized": "Надіслано запит на очищення індексу надісланих файлів, але не авторизовано. Для цього надайте аргумент '--confirm'", "cleanup_index_unathorized": "Надіслано запит на очищення індексу надісланих файлів, але не авторизовано. Для цього надайте аргумент '--confirm'"
"random_pic_response": "Відповідь на пошук випадкової картинки: {0}",
"random_pic_error_code": "Не вдалося отримати фото з альбому {0}: HTTP {1}",
"random_pic_error_debug": "Не вдалося отримати фотографії з '{0}/albums/{1}/photos?q=&page_size={2}&caption=queue', використовуючи токен '{3}': HTTP {4}",
"find_pic_error": "Не вдалося знайти зображення з назвою '{0}' та підписом '{1}' через: {2}",
"pic_upload_error": "Не вдалося завантажити '{0}' до API: HTTP {1} з повідомленням '{2}'",
"api_creds_invalid": "Невірні облікові дані API! Не вдалося увійти в '{0}' за допомогою логіна '{1}': HTTP {2}",
"user_blocked": "Користувача {0} було заблоковано",
"user_unblocked": "Користувача {0} було розблоковано",
"submission_accepted": "Подання з ID '{0}' прийнято та завантажено з ID '{1}'",
"submission_rejected": "Подання з ID '{0}' відхилено",
"submission_duplicate": "Подання з ID '{0}' не може бути прийнято через наявність дублікатів: {1}"
} }
} }

View File

@@ -8,19 +8,16 @@ from traceback import print_exc
from typing import Tuple, Union from typing import Tuple, Union
import aiofiles import aiofiles
from aiohttp import FormData from aiohttp import ClientSession, FormData
from ujson import dumps
from classes.exceptions import ( from classes.exceptions import SubmissionUploadError
AlbumCreationDuplicateError,
AlbumCreationError,
AlbumCreationNameError,
SubmissionUploadError,
UserCreationDuplicateError,
UserCreationError,
)
from modules.logger import logWrite from modules.logger import logWrite
from modules.utils import configGet, locale from modules.utils import configGet
from modules.http_client import http_session
http_session = ClientSession(
json_serialize=dumps,
)
async def authorize() -> str: async def authorize() -> str:
@@ -48,15 +45,7 @@ async def authorize() -> str:
) )
if not response.ok: if not response.ok:
logWrite( logWrite(
locale( f'Incorrect API credentials! Could not login into "{configGet("address", "posting", "api")}" using login "{configGet("username", "posting", "api")}": HTTP {response.status}'
"api_creds_invalid",
"console",
locale=configGet("locale_log").format(
configGet("address", "posting", "api"),
configGet("username", "posting", "api"),
response.status,
),
)
) )
raise ValueError raise ValueError
async with aiofiles.open( async with aiofiles.open(
@@ -82,34 +71,13 @@ async def random_pic(token: Union[str, None] = None) -> Tuple[str, str]:
f'{configGet("address", "posting", "api")}/albums/{configGet("album", "posting", "api")}/photos?q=&page_size={configGet("page_size", "posting")}&caption=queue', 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}"}, headers={"Authorization": f"Bearer {token}"},
) )
logWrite( print(await resp.json(), flush=True)
locale("random_pic_response", "console", locale=configGet("locale_log")).format(
await resp.json()
),
debug=True,
)
if resp.status != 200: if resp.status != 200:
logWrite( logWrite(
locale( f'Could not get photos from album {configGet("album", "posting", "api")}: HTTP {resp.status}'
"random_pic_error_code",
"console",
locale=configGet("locale_log").format(
configGet("album", "posting", "api"), resp.status
),
),
) )
logWrite( logWrite(
locale( f'Could not get photos from "{configGet("address", "posting", "api")}/albums/{configGet("album", "posting", "api")}/photos?q=&page_size={configGet("page_size", "posting")}&caption=queue" using token "{token}": HTTP {resp.status}',
"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, debug=True,
) )
raise ValueError raise ValueError
@@ -121,7 +89,7 @@ async def random_pic(token: Union[str, None] = None) -> Tuple[str, str]:
async def upload_pic( async def upload_pic(
filepath: str, allow_duplicates: bool = False, token: Union[str, None] = None filepath: str, allow_duplicates: bool = False, token: Union[str, None] = None
) -> Tuple[bool, list, Union[str, None]]: ) -> Tuple[bool, list]:
token = await authorize() if token is None else token token = await authorize() if token is None else token
try: try:
pic_name = path.basename(filepath) pic_name = path.basename(filepath)
@@ -142,36 +110,28 @@ async def upload_pic(
headers={"Authorization": f"Bearer {token}"}, headers={"Authorization": f"Bearer {token}"},
data=formdata, data=formdata,
) )
response_json = await response.json()
if response.status != 200 and response.status != 409: if response.status != 200 and response.status != 409:
logWrite( logWrite(
locale( f"Could not upload '{filepath}' to API: HTTP {response.status} with message '{response.content}'"
"pic_upload_error",
"console",
locale=configGet("locale_log").format(
filepath, response.status, response.content
),
),
) )
raise SubmissionUploadError( raise SubmissionUploadError(
str(filepath), response.status, response.content str(filepath), response.status, response.content
) )
id = response_json["id"] if "id" in await response.json() else None
duplicates = [] duplicates = []
if "duplicates" in response_json: if "duplicates" in (await response.json()):
for index, duplicate in enumerate(response_json["duplicates"]): # type: ignore for index, duplicate in enumerate((await response.json())["duplicates"]):
if response_json["access_token"] is None: if (await response.json())["access_token"] is None:
duplicates.append( duplicates.append(
f'`{duplicate["id"]}`:\n{configGet("address_external", "posting", "api")}/photos/{duplicate["id"]}' f'`{duplicate["id"]}`:\n{configGet("address_external", "posting", "api")}/photos/{duplicate["id"]}'
) )
else: else:
duplicates.append( duplicates.append(
f'`{duplicate["id"]}`:\n{configGet("address_external", "posting", "api")}/token/photo/{response_json["access_token"]}?id={index}' f'`{duplicate["id"]}`:\n{configGet("address_external", "posting", "api")}/token/photo/{(await response.json())["access_token"]}?id={index}'
) )
return True, duplicates, id return True, duplicates
except Exception as exp: except Exception as exp:
print_exc() print_exc()
return False, [], None return False, []
async def find_pic( async def find_pic(
@@ -192,11 +152,7 @@ async def find_pic(
return (await response.json())["results"] return (await response.json())["results"]
except Exception as exp: except Exception as exp:
logWrite( logWrite(
locale( f"Could not find image with name '{name}' and caption '{caption}' due to: {exp}"
"find_pic_error",
"console",
locale=configGet("locale_log").format(name, caption, exp),
),
) )
return None return None
@@ -204,60 +160,14 @@ async def find_pic(
async def move_pic(id: str, token: Union[str, None] = None) -> bool: async def move_pic(id: str, token: Union[str, None] = None) -> bool:
token = await authorize() if token is None else token token = await authorize() if token is None else token
try: try:
response = await http_session.patch( await http_session.patch(
f'{configGet("address", "posting", "api")}/photos/{id}?caption=sent', f'{configGet("address", "posting", "api")}/photos/{id}?caption=sent',
headers={"Authorization": f"Bearer {token}"}, headers={"Authorization": f"Bearer {token}"},
) )
if response.status != 200:
logWrite(f"Media moving failed with HTTP {response.status}", debug=True)
return False
return True return True
except: except:
return False 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
if __name__ == "__main__": if __name__ == "__main__":
print(asyncio.run(authorize())) print(asyncio.run(authorize()))

View File

@@ -1,6 +1,5 @@
from modules.utils import configGet from modules.utils import configGet
from classes.poster_client import PosterClient from classes.poster_client import PosterClient
from convopyro import Conversation
app = PosterClient( app = PosterClient(
"duptsiaposter", "duptsiaposter",
@@ -8,7 +7,3 @@ app = PosterClient(
api_id=configGet("api_id", "bot"), api_id=configGet("api_id", "bot"),
api_hash=configGet("api_hash", "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,6 +0,0 @@
from aiohttp import ClientSession
from ujson import dumps
http_session = ClientSession(
json_serialize=dumps,
)

View File

@@ -1,6 +1,5 @@
from datetime import datetime from datetime import datetime
from os import makedirs, path from os import makedirs, path
from random import choice
from shutil import rmtree from shutil import rmtree
from traceback import format_exc from traceback import format_exc
from uuid import uuid4 from uuid import uuid4
@@ -130,9 +129,9 @@ async def send_content(app: PosterClient) -> None:
if configGet("enabled", "caption"): if configGet("enabled", "caption"):
if configGet("link", "caption") != None: if configGet("link", "caption") != None:
caption = f"{caption}[{choice(configGet('text', 'caption'))}]({configGet('link', 'caption')})" caption = f"{caption}[{configGet('text', 'caption')}]({configGet('link', 'caption')})"
else: else:
caption = f"{caption}{choice(configGet('text', 'caption'))}" caption = f"{caption}{configGet('text', 'caption')}"
else: else:
caption = caption caption = caption

View File

@@ -1,96 +1,14 @@
from os import kill, makedirs from os import kill
from os import name as osname from os import name as osname
from os import path, sep from os import sep
from sys import exit from sys import exit
from traceback import print_exc from traceback import print_exc
from typing import Any from typing import Any
from zipfile import ZipFile
import aiofiles
from ujson import JSONDecodeError, dumps, loads from ujson import JSONDecodeError, dumps, loads
from modules.logger import logWrite from modules.logger import logWrite
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"],
}
def jsonLoad(filename: str) -> Any: def jsonLoad(filename: str) -> Any:
"""Loads arg1 as json and returns its contents""" """Loads arg1 as json and returns its contents"""
@@ -115,11 +33,7 @@ def jsonSave(contents: Any, filename: str) -> None:
"""Dumps dict/list arg1 to file arg2""" """Dumps dict/list arg1 to file arg2"""
try: try:
with open(filename, "w", encoding="utf8") as file: with open(filename, "w", encoding="utf8") as file:
file.write( file.write(dumps(contents, ensure_ascii=False, indent=4))
dumps(
contents, ensure_ascii=False, indent=4, escape_forward_slashes=False
)
)
file.close() file.close()
except Exception as exp: except Exception as exp:
logWrite(f"Could not save json file {filename}: {exp}\n{print_exc()}") logWrite(f"Could not save json file {filename}: {exp}\n{print_exc()}")
@@ -155,20 +69,9 @@ def configGet(key: str, *args: str):
* any: Value of provided key * any: Value of provided key
""" """
this_dict = jsonLoad("config.json") this_dict = jsonLoad("config.json")
try: this_key = this_dict
this_key = this_dict for dict_key in args:
for dict_key in args: this_key = this_key[dict_key]
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] return this_key[key]
@@ -209,28 +112,6 @@ def locale(key: str, *args: str, locale=configGet("locale")):
return f'⚠️ Locale in config is invalid: could not get "{key}" in {str(args)} from locale "{locale}"' 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):
"""Extract and save file from archive
Args:
* handle (ZipFile): ZipFile handler
* filename (str): File base name
* path (str): Path where to store
"""
data = handle.read(filename)
filepath = path.join(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)
except IsADirectoryError:
makedirs(filepath, exist_ok=True)
except FileNotFoundError:
pass
return
try: try:
from psutil import Process from psutil import Process
except ModuleNotFoundError: except ModuleNotFoundError:

View File

@@ -1,29 +0,0 @@
from os import getpid, makedirs, path
from time import time
from modules.app import app
from pyrogram import filters
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
@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()

View File

@@ -7,7 +7,6 @@ from classes.poster_client import PosterClient
from classes.user import PosterUser from classes.user import PosterUser
from modules.app import app from modules.app import app
from modules.logger import logWrite
from modules.utils import configGet, locale from modules.utils import configGet, locale
from modules.database import col_submitted from modules.database import col_submitted
from bson import ObjectId from bson import ObjectId
@@ -39,22 +38,11 @@ async def callback_query_yes(app: PosterClient, clb: CallbackQuery):
), ),
quote=True, quote=True,
) )
logWrite(
locale(
"submission_duplicate",
"console",
locale=configGet("locale_log").format(
fullclb[2],
str(exp.duplicates),
),
),
debug=True,
)
return return
if submission[0] is not None: if submission is not None:
await submission[0].reply_text( await submission.reply_text(
locale("sub_yes", "message", locale=submission[0].from_user.language_code), locale("sub_yes", "message", locale=submission.from_user.language_code),
quote=True, quote=True,
) )
elif db_entry is not None: elif db_entry is not None:
@@ -85,25 +73,10 @@ async def callback_query_yes(app: PosterClient, clb: CallbackQuery):
] ]
] ]
) )
if configGet("send_uploaded_id", "submission"):
await clb.message.edit_caption(
clb.message.caption + f"\n\nID: `{submission[1]}`"
)
await clb.message.edit_reply_markup( await clb.message.edit_reply_markup(
reply_markup=InlineKeyboardMarkup(edited_markup) reply_markup=InlineKeyboardMarkup(edited_markup)
) )
logWrite(
locale(
"submission_accepted",
"console",
locale=configGet("locale_log").format(fullclb[2], submission[1]),
),
debug=True,
)
# try: # try:
# if configGet("api_based", "mode") is True: # if configGet("api_based", "mode") is True:
# media = await app.download_media(submission, file_name=configGet("tmp", "locations")+sep) # media = await app.download_media(submission, file_name=configGet("tmp", "locations")+sep)
@@ -194,14 +167,6 @@ async def callback_query_no(app: PosterClient, clb: CallbackQuery):
await clb.message.edit_reply_markup( await clb.message.edit_reply_markup(
reply_markup=InlineKeyboardMarkup(edited_markup) reply_markup=InlineKeyboardMarkup(edited_markup)
) )
logWrite(
locale(
"submission_rejected",
"console",
locale=configGet("locale_log").format(fullclb[2]),
),
debug=True,
)
@app.on_callback_query(filters.regex("sub_block_[\s\S]*")) @app.on_callback_query(filters.regex("sub_block_[\s\S]*"))
@@ -229,14 +194,6 @@ async def callback_query_block(app: PosterClient, clb: CallbackQuery):
await clb.message.edit_reply_markup( await clb.message.edit_reply_markup(
reply_markup=InlineKeyboardMarkup(edited_markup) reply_markup=InlineKeyboardMarkup(edited_markup)
) )
logWrite(
locale(
"user_blocked",
"console",
locale=configGet("locale_log").format(fullclb[2]),
),
debug=True,
)
@app.on_callback_query(filters.regex("sub_unblock_[\s\S]*")) @app.on_callback_query(filters.regex("sub_unblock_[\s\S]*"))
@@ -264,11 +221,3 @@ async def callback_query_unblock(app: PosterClient, clb: CallbackQuery):
await clb.message.edit_reply_markup( await clb.message.edit_reply_markup(
reply_markup=InlineKeyboardMarkup(edited_markup) reply_markup=InlineKeyboardMarkup(edited_markup)
) )
logWrite(
locale(
"user_unblocked",
"console",
locale=configGet("locale_log").format(fullclb[2]),
),
debug=True,
)

View File

@@ -1,45 +1,24 @@
from os import getpid, makedirs, path from os import getpid
from time import time
from pyrogram import filters from pyrogram import filters
from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
from classes.poster_client import PosterClient from classes.poster_client import PosterClient
from modules.app import app, users_with_context from pyrogram.types import Message
from modules.app import app
from modules.logger import logWrite from modules.logger import logWrite
from modules.scheduler import scheduler from modules.utils import configGet, killProc, locale
from modules.utils import configGet, jsonSave, locale
@app.on_message(~filters.scheduled & filters.command(["shutdown"], prefixes=["", "/"])) @app.on_message(
~filters.scheduled & filters.command(["kill", "die", "reboot"], prefixes=["", "/"])
)
async def cmd_kill(app: PosterClient, msg: Message): async def cmd_kill(app: PosterClient, msg: Message):
if msg.from_user.id in app.admins: 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() pid = getpid()
logWrite(f"Shutting down bot with pid {pid}") logWrite(
locale("shutdown", "console", locale=configGet("locale")).format(str(pid))
)
await msg.reply_text( await msg.reply_text(
locale("shutdown", "message", locale=msg.from_user.language_code).format( locale("shutdown", "message", locale=configGet("locale")).format(str(pid))
pid
),
) )
scheduler.shutdown() killProc(pid)
makedirs(configGet("cache", "locations"), exist_ok=True)
jsonSave(
{"timestamp": time()},
path.join(configGet("cache", "locations"), "shutdown_time"),
)
exit()

View File

@@ -1,160 +1,14 @@
import asyncio
from glob import iglob
from os import getcwd, makedirs, path, remove
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 pyrogram import filters from pyrogram import filters
from pyrogram.types import Message from pyrogram.types import Message
from classes.poster_client import PosterClient from classes.poster_client import PosterClient
from modules.api_client import remove_pic, upload_pic from modules.app import app
from modules.app import app, users_with_context
from modules.logger import logWrite
from modules.utils import configGet, extract_and_save, locale
@app.on_message(~filters.scheduled & filters.command(["import"], prefixes=["", "/"])) @app.on_message(~filters.scheduled & filters.command(["import"], prefixes=["", "/"]))
async def cmd_import(app: PosterClient, msg: Message): async def cmd_import(app: PosterClient, msg: Message):
if msg.from_user.id in app.admins: if msg.from_user.id in app.admins:
global users_with_context pass
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("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),
quote=True,
)
return
@app.on_message(~filters.scheduled & filters.command(["export"], prefixes=["", "/"])) @app.on_message(~filters.scheduled & filters.command(["export"], prefixes=["", "/"]))
@@ -166,46 +20,7 @@ async def cmd_export(app: PosterClient, msg: Message):
@app.on_message(~filters.scheduled & filters.command(["remove"], prefixes=["", "/"])) @app.on_message(~filters.scheduled & filters.command(["remove"], prefixes=["", "/"]))
async def cmd_remove(app: PosterClient, msg: Message): async def cmd_remove(app: PosterClient, msg: Message):
if msg.from_user.id in app.admins: if msg.from_user.id in app.admins:
global users_with_context pass
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)
)
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("remove_ignored", "message", locale=msg.from_user.language_code),
quote=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)
)
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)
)
@app.on_message(~filters.scheduled & filters.command(["purge"], prefixes=["", "/"])) @app.on_message(~filters.scheduled & filters.command(["purge"], prefixes=["", "/"]))

View File

@@ -4,17 +4,17 @@ from traceback import format_exc
from uuid import uuid4 from uuid import uuid4
from pyrogram import filters from pyrogram import filters
from pyrogram.enums.chat_action import ChatAction
from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
from pyrogram.enums.chat_action import ChatAction
from classes.enums.submission_types import SubmissionType
from classes.exceptions import SubmissionDuplicatesError from classes.exceptions import SubmissionDuplicatesError
from classes.poster_client import PosterClient from classes.poster_client import PosterClient
from classes.user import PosterUser from classes.user import PosterUser
from modules.app import app, users_with_context
from modules.app import app
from modules.database import col_banned, col_submitted from modules.database import col_banned, col_submitted
from modules.logger import logWrite from modules.logger import logWrite
from modules.utils import configGet, locale from modules.utils import configGet, locale
from classes.enums.submission_types import SubmissionType
@app.on_message( @app.on_message(
@@ -24,9 +24,6 @@ from modules.utils import configGet, locale
| filters.document | filters.document
) )
async def get_submission(app: PosterClient, msg: Message): async def get_submission(app: PosterClient, msg: Message):
global users_with_context
if msg.from_user.id in users_with_context:
return
try: try:
if col_banned.find_one({"user": msg.from_user.id}) is not None: if col_banned.find_one({"user": msg.from_user.id}) is not None:
return return
@@ -46,15 +43,9 @@ async def get_submission(app: PosterClient, msg: Message):
return return
if msg.document is not None: 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,
)
if msg.document.mime_type not in configGet("mime_types", "submission"): if msg.document.mime_type not in configGet("mime_types", "submission"):
await msg.reply_text( await msg.reply_text(
locale("mime_not_allowed", "message", locale=user_locale).format( locale("mime_not_allowed", "message", locale=user_locale),
", ".join(configGet("mime_types", "submission"))
),
quote=True, quote=True,
) )
return return
@@ -74,10 +65,6 @@ async def get_submission(app: PosterClient, msg: Message):
) # , msg.document.file_name ) # , msg.document.file_name
if msg.video is not None: 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,
)
if msg.video.file_size > configGet("file_size", "submission"): if msg.video.file_size > configGet("file_size", "submission"):
await msg.reply_text( await msg.reply_text(
locale("document_too_large", "message", locale=user_locale).format( locale("document_too_large", "message", locale=user_locale).format(
@@ -91,10 +78,6 @@ async def get_submission(app: PosterClient, msg: Message):
contents = msg.video.file_id, SubmissionType.VIDEO # , msg.video.file_name contents = msg.video.file_id, SubmissionType.VIDEO # , msg.video.file_name
if msg.animation is not None: if msg.animation is not None:
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,
)
if msg.animation.file_size > configGet("file_size", "submission"): if msg.animation.file_size > configGet("file_size", "submission"):
await msg.reply_text( await msg.reply_text(
locale("document_too_large", "message", locale=user_locale).format( locale("document_too_large", "message", locale=user_locale).format(
@@ -111,10 +94,6 @@ async def get_submission(app: PosterClient, msg: Message):
) # , msg.animation.file_name ) # , msg.animation.file_name
if msg.photo is not None: 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,
)
contents = msg.photo.file_id, SubmissionType.PHOTO # , "please_generate" contents = msg.photo.file_id, SubmissionType.PHOTO # , "please_generate"
if save_tmp is not None: if save_tmp is not None:
@@ -209,14 +188,12 @@ async def get_submission(app: PosterClient, msg: Message):
and configGet("admins", "submission", "require_confirmation") is False and configGet("admins", "submission", "require_confirmation") is False
): ):
try: try:
submitted = await app.submit_photo(str(inserted.inserted_id)) await app.submit_photo(str(inserted.inserted_id))
await msg.reply_text( await msg.reply_text(
locale("sub_yes_auto", "message", locale=user_locale), locale("sub_yes_auto", "message", locale=user_locale),
disable_notification=True, disable_notification=True,
quote=True, quote=True,
) )
if configGet("send_uploaded_id", "submission"):
caption += f"\n\nID: `{submitted[1]}`"
await msg.copy(app.owner, caption=caption, disable_notification=True) await msg.copy(app.owner, caption=caption, disable_notification=True)
return return
except SubmissionDuplicatesError as exp: except SubmissionDuplicatesError as exp:
@@ -235,14 +212,12 @@ async def get_submission(app: PosterClient, msg: Message):
and configGet("users", "submission", "require_confirmation") is False and configGet("users", "submission", "require_confirmation") is False
): ):
try: try:
submitted = await app.submit_photo(str(inserted.inserted_id)) await app.submit_photo(str(inserted.inserted_id))
await msg.reply_text( await msg.reply_text(
locale("sub_yes_auto", "message", locale=user_locale), locale("sub_yes_auto", "message", locale=user_locale),
disable_notification=True, disable_notification=True,
quote=True, quote=True,
) )
if configGet("send_uploaded_id", "submission"):
caption += f"\n\nID: `{submitted[1]}`"
await msg.copy(app.owner, caption=caption) await msg.copy(app.owner, caption=caption)
return return
except SubmissionDuplicatesError as exp: except SubmissionDuplicatesError as exp:

250
poster.py
View File

@@ -1,23 +1,94 @@
from datetime import datetime from os import sep, remove, getpid
from os import getpid, path from shutil import move
from sys import exit from sys import exit
from time import time from argparse import ArgumentParser
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.logger import logWrite
from modules.scheduler import scheduler from modules.scheduler import scheduler
from modules.utils import configGet, jsonLoad, jsonSave, locale from modules.utils import configGet, jsonLoad, jsonSave, killProc, locale
# Args =====================================================================================================================================
parser = ArgumentParser(
prog="Telegram Poster",
description="Bot for posting some of your stuff and also receiving submissions.",
)
parser.add_argument("-m", "--move-sent", action="store_true")
parser.add_argument("-c", "--cleanup", action="store_true")
parser.add_argument("--confirm", action="store_true")
parser.add_argument("-i", "--cleanup-index", action="store_true")
parser.add_argument("-n", "--norun", action="store_true")
args = parser.parse_args()
if args.move_sent:
for entry in jsonLoad(configGet("index", "locations"))["sent"]:
try:
move(
configGet("queue", "locations") + sep + entry,
configGet("sent", "locations") + sep + entry,
)
except FileNotFoundError:
logWrite(
locale(
"move_sent_doesnt_exist", "console", locale=configGet("locale")
).format(entry)
)
except Exception as exp:
logWrite(
locale(
"move_sent_doesnt_exception", "console", locale=configGet("locale")
).format(entry, exp)
)
logWrite(locale("move_sent_completed", "console", locale=configGet("locale")))
if args.cleanup:
if args.confirm:
index = jsonLoad(configGet("index", "locations"))
for entry in index["sent"]:
try:
try:
remove(configGet("queue", "locations") + sep + entry)
except FileNotFoundError:
pass
try:
remove(configGet("sent", "locations") + sep + entry)
except FileNotFoundError:
pass
except Exception as exp:
logWrite(
locale(
"cleanup_exception", "console", locale=configGet("locale")
).format(entry, exp)
)
jsonSave(index, jsonLoad(configGet("index", "locations")))
logWrite(locale("cleanup_completed", "console", locale=configGet("locale")))
else:
logWrite(locale("cleanup_unathorized", "console", locale=configGet("locale")))
if args.cleanup_index:
if args.confirm:
index = jsonLoad(configGet("index", "locations"))
index["sent"] = []
jsonSave(index, jsonLoad(configGet("index", "locations")))
logWrite(
locale("cleanup_index_completed", "console", locale=configGet("locale"))
)
else:
logWrite(
locale("cleanup_index_unathorized", "console", locale=configGet("locale"))
)
if args.norun:
logWrite(locale("passed_norun", "console", locale=configGet("locale_log")))
exit()
# ===========================================================================================================================================
# Import =================================================================================================================================== # Import ===================================================================================================================================
try: try:
from dateutil.relativedelta import relativedelta
from pyrogram.errors import bad_request_400
from pyrogram.sync import idle
from modules.app import app from modules.app import app
from pyrogram.sync import idle
except ModuleNotFoundError: except ModuleNotFoundError:
print(locale("deps_missing", "console", locale=configGet("locale")), flush=True) print(locale("deps_missing", "console", locale=configGet("locale")), flush=True)
exit() exit()
@@ -25,7 +96,6 @@ except ModuleNotFoundError:
pid = getpid() pid = getpid()
version = 0.1
# Work in progress # Work in progress
# def check_forwards(app): # def check_forwards(app):
@@ -63,8 +133,6 @@ version = 0.1
# check_forwards(app) # check_forwards(app)
from plugins.callbacks.shutdown import *
# Imports ================================================================================================================================== # Imports ==================================================================================================================================
from plugins.commands.general import * from plugins.commands.general import *
@@ -74,9 +142,6 @@ if configGet("submit", "mode"):
from plugins.commands.mode_submit import * from plugins.commands.mode_submit import *
from plugins.handlers.submission import * from plugins.handlers.submission import *
if configGet("post", "mode"):
from plugins.commands.photos import *
# if configGet("api_based", "mode"): # if configGet("api_based", "mode"):
# from modules.api_client import authorize # from modules.api_client import authorize
# =========================================================================================================================================== # ===========================================================================================================================================
@@ -121,146 +186,31 @@ if configGet("post", "mode"):
# uvloop.install() # uvloop.install()
# asyncio.run(main()) # asyncio.run(main())
if __name__ == "__main__":
logWrite(locale("startup", "console", locale=configGet("locale")).format(str(pid)))
async def main(): app.start()
logWrite(locale("startup", "console").format(str(pid)))
await app.start()
if configGet("startup", "reports"): if configGet("startup", "reports"):
try: app.send_message(
if path.exists(path.join(configGet("cache", "locations"), "shutdown_time")): app.owner,
downtime = relativedelta( locale("startup", "message", locale=configGet("locale")).format(str(pid)),
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"): if configGet("post", "mode"):
scheduler.start() scheduler.start()
try: # if configGet("api_based", "mode"):
token = await authorize() # token = await 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}"})
if ( idle()
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() app.send_message(
app.owner,
try: locale("shutdown", "message", locale=configGet("locale")).format(str(pid)),
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", locale=configGet("locale")).format(str(pid)))
logWrite(locale("shutdown", "console").format(str(pid))) killProc(pid)
scheduler.shutdown()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

View File

@@ -0,0 +1 @@
tgcrypto~=1.2.5

View File

@@ -1,12 +1,9 @@
python_dateutil==2.8.2 apscheduler~=3.10.1
apscheduler==3.10.1 pyrogram~=2.0.101
pytimeparse==1.1.8 pytimeparse~=1.1.8
convopyro==0.5
pyrogram==2.0.104
aiofiles~=23.1.0 aiofiles~=23.1.0
tgcrypto==1.2.5
aiohttp~=3.8.4 aiohttp~=3.8.4
psutil==5.9.5 psutil~=5.9.4
pymongo==4.3.3 pymongo~=4.3.3
pillow~=9.5.0 pillow~=9.4.0
ujson==5.7.0 ujson~=5.7.0