31 Commits

Author SHA1 Message Date
853c3c7cea dev (#19)
* Config file changes
* Commands `/remove`, `/import` and `/export`

Co-authored-by: profitroll <vozhd.kk@gmail.com>
Co-authored-by: Profitroll <47523801+profitrollgame@users.noreply.github.com>
Co-authored-by: Renovate <renovate@git.end-play.xyz>
Reviewed-on: #19
2023-04-24 13:48:22 +03:00
e5f80d9702 Updated Renovate config 2023-04-20 13:41:14 +02:00
6f03d5c18f Configure Renovate (#14)
Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboarding PR to help you understand and configure settings before regular Pull Requests begin.

🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged.

---
### Detected Package Files

 * `requirements.txt` (pip_requirements)

### Configuration Summary

Based on the default config's presets, Renovate will:

  - Start dependency updates only once this onboarding PR is merged
  - Enable Renovate Dependency Dashboard creation.
  - Use semantic commit type `fix` for dependencies and `chore` for all others if semantic commits are in use.
  - Ignore `node_modules`, `bower_components`, `vendor` and various test/tests directories.
  - Group known monorepo packages together.
  - Use curated list of recommended non-monorepo package groupings.
  - Apply crowd-sourced package replacement rules.
  - Apply crowd-sourced workarounds for known problems with packages.

🔡 Would you like to change the way Renovate is upgrading your dependencies? Simply edit the `.renovaterc` in this branch with your custom config and the list of Pull Requests in the "What to Expect" section below will be updated the next time Renovate runs.

---

### What to Expect

It looks like your repository dependencies are already up-to-date and no Pull Requests will be necessary right away.

---

 Got questions? Check out Renovate's [Docs](https://docs.renovatebot.com/), particularly the Getting Started section.
If you need any further assistance then you can also [request help here](https://github.com/renovatebot/renovate/discussions).

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).

Reviewed-on: #14
Co-authored-by: Renovate <renovate@git.end-play.xyz>
Co-committed-by: Renovate <renovate@git.end-play.xyz>
2023-04-20 14:40:36 +03:00
5b1afe2c9e WIP: API usage as main 2023-02-15 14:06:06 +01:00
6eaa6019a3 Added tmp file size 2023-02-15 11:56:15 +01:00
493be818b8 Updated ignore 2023-02-15 11:56:05 +01:00
cf09017985 TMP: interval changes 2023-02-14 16:26:08 +01:00
7940cbe5a7 WIP: API usage as main 2023-02-14 16:25:56 +01:00
8e0fee4cb9 Added database support 2023-02-14 14:45:52 +01:00
08e03ba911 Moved handlers to another location 2023-02-14 11:38:54 +01:00
b589da5d8f Updated dependencies 2023-02-14 11:38:09 +01:00
a3a1be241c Small keyboard fix 2023-01-17 14:38:31 +01:00
9ce05589b0 Contextual keyboard [WIP] 2023-01-17 14:38:21 +01:00
c1ee988106 Added empty callback 2023-01-17 14:37:36 +01:00
aebe804b58 Optimized for API 2023-01-17 14:11:23 +01:00
9f0ed202da Updated ignore 2023-01-17 12:51:30 +01:00
b4a9595eb4 Renamed logging module 2023-01-17 11:43:10 +01:00
569fffd223 Updated requirements 2023-01-17 11:41:07 +01:00
f719befe40 Updated config 2023-01-17 11:40:59 +01:00
c4323f0b00 Moved to async 2023-01-10 13:06:24 +01:00
6facf428c5 Complete refactor 2023-01-10 12:52:44 +01:00
9261585f5f Added scheduler start 2023-01-09 19:01:07 +02:00
cddf1e211a Moved to apscheduler 2023-01-09 15:36:56 +01:00
cd5b73e7ff Small improvements 2022-11-03 12:13:02 +01:00
46ea9a2f74 Merge branch 'master' of https://git.profitroll.eu/profitroll/TelegramPoster 2022-11-03 12:12:41 +01:00
7a7f386891 Is now using argparse library 2022-11-03 12:10:59 +01:00
Profitroll
11255bbbd0 Pagination fixed 2022-10-05 21:37:37 +02:00
Profitroll
8ae50a4f97 Low number of files in queue reminder 2022-10-05 21:36:59 +02:00
Profitroll
46e3665749 Small fix 2022-09-24 17:03:20 +02:00
Profitroll
0e14ca600f Changed some imports' paths 2022-09-18 18:27:25 +02:00
Profitroll
c4177d1575 Optimized imports 2022-09-18 18:27:13 +02:00
42 changed files with 2519 additions and 607 deletions

5
.gitignore vendored
View File

@@ -154,7 +154,6 @@ cython_debug/
# ---> VisualStudioCode # ---> VisualStudioCode
.vscode/* .vscode/*
!.vscode/settings.json
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
@@ -167,3 +166,7 @@ cython_debug/
*.vsix *.vsix
# Project # Project
cache
data
logs
config.json

20
.renovaterc Normal file
View File

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

112
README.md
View File

@@ -1,60 +1,110 @@
# TelegramPoster <h1 align="center">TelegramPoster</h1>
> Шукаєш інструкцію українською? А вона [ось тут](https://git.end-play.xyz/profitroll/TelegramPoster/src/branch/master/README_uk.md) знаходиться)
<p align="center">
<a href="https://git.end-play.xyz/profitroll/TelegramPoster/src/branch/dev/LICENSE"><img alt="License: GPL" src="https://img.shields.io/badge/License-GPL-blue"></a>
<a href="https://git.end-play.xyz/profitroll/TelegramPoster"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
</p>
> Шукаєш інструкцію українською? А вона [ось тут](https://git.end-play.xyz/profitroll/TelegramPoster/src/branch/dev/README_uk.md) знаходиться)
This bot is used for one and only task - post pictures from my personal archive. Here's its source code so you can also host a bot and have fun with it. Just don't exepect it to be brilliant. It is not. But hey, you can always fork it ;) This bot is used for one and only task - post pictures from my personal archive. Here's its source code so you can also host a bot and have fun with it. Just don't exepect it to be brilliant. It is not. But hey, you can always fork it ;)
## Dependencies
* [Python 3.7+](https://www.python.org) (3.9+ recommended)
* [MongoDB](https://www.mongodb.com)
* [PhotosAPI](https://git.end-play.xyz/profitroll/PhotosAPI)
Use [MongoDB's installation manual](https://www.mongodb.com/docs/manual/installation) and [Photos API's README](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md).
Please note that Photos API also requires MongoDB so it makes sense to install and configure Mongo first.
## Installation ## Installation
To make this bot run at first you need to have a Python interpreter and git. Google is your friend finding it. You can also ignore git and simply download source code, should also work fine. After that you're ready to go.
To make this bot run at first you need to have a Python interpreter, Photos API, MongoDB and optionally git. You can also ignore git and simply download source code, should also work fine. After that you're ready to go.
> In this README I assume that you're using default python in your > In this README I assume that you're using default python in your
> system and your system's PATH contains it. If your default python > system and your system's PATH contains it. If your default python
> is `python3` or for example `/home/user/.local/bin/python3.9` - use it instead. > is `python3` or for example `/home/user/.local/bin/python3.9` - use it instead.
> If it's non-standart executable path - you should also change > If it's non-standard executable path - you should also change
> it in scripts you will use (`loop.sh`, `loop.bat`, `start.sh` and `start.bat`). > it in scripts you will use (`loop.sh`, `loop.bat`, `start.sh` and `start.bat`).
1. Download the bot: 1. Install Mongo and Photos API:
1. `git clone https://git.end-play.xyz/profitroll/TelegramSender.git` (if you want to use git)
2. `cd ./TelegramSender` 1. Install MongoDB by following [official installation manual](https://www.mongodb.com/docs/manual/installation)
2. Install Photos API by following [Photos API's README](https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md)
2. Download the bot:
1. `git clone -b dev https://git.end-play.xyz/profitroll/TelegramPoster.git` (if you're using git)
2. `cd TelegramPoster`
3. Create virtual environment [Optional]:
1. Install virtualenv module: `pip install virtualenv`
2. Create venv: `python -m venv 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:
2. Install dependencies:
`python -m pip install -r requirements.txt` `python -m pip install -r requirements.txt`
Without installing those - bot cannot work at all Without installing those - bot cannot work at all.
3. Install optional dependencies [Not required]: 5. Configure "bot" and "owner" with your favorite text editor:
`python -m pip install -r requirements-optional.txt`
These are not required but can make the bot run a bit faster
4. Configure your bot with a favorite text editor: 1. Copy file `config_example.json` to `config.json`
`nano config.json` 2. Open `config.json` using your favorite text editor. For example `nano config.json`, but you can edit with vim, nano, on Windows it's Notepad or Notepad++. Whatever
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.
If you don't know where to find bot_token and your id - here you can find some hints: [get bot token](https://www.siteguarding.com/en/how-to-get-telegram-bot-api-token), [get your id](https://www.alphr.com/telegram-find-user-id/), [get api_hash and api_id](https://core.telegram.org/api/obtaining_api_id).
Also don't forget to change bot's working mode. Dict key `"mode"` contains keys `"post"` and `"submit"`, each of those can be either `true` or `false`.
5. Add bot to the channel: If you don't know where to find bot_token and your id - here you can find some hints: [get bot token](https://www.siteguarding.com/en/how-to-get-telegram-bot-api-token), [get your id](https://www.alphr.com/telegram-find-user-id), [get api_hash and api_id](https://core.telegram.org/api/obtaining_api_id).
To use your bot of course you need to have a channel or group otherwise makes no sense to have such a bot. [Here](https://stackoverflow.com/a/33497769) you can find a quick guide how to add your bot to a channel.
6. Fill your contents folder: 6. Configure database and API:
Of course bot cannot post something from nothing. Configure your `config.json` what media types bot should post (`"posting", "extensions"`), when to post them (`"posting", "time"`) and also where to find them (`"locations"`). You can also move them when sent by setting `"posting", "move_sent"` to `true`.
6. Good to go, run it! 1. Configure database:
`python ./main.py` 1. Change database host and port in keys `"database.host"` and `"database.port"`. For default local installation those will be `127.0.0.1` and `27017` respectively
2. Change database name to the one you like in `"database.name"`. It will be automatically created on start
3. If you've changed user and password to access the db, you should also change `"database.user"` and `"database.password"` keys, otherwise leave them `null` (default).
2. Configure Photos API:
1. Change `"posting.api.address"` to the one your API servers uses
2. Run your bot using `python poster.py --create-user --create-album` to configure its new user and album. You can also use manual user and album creation described [in the wiki](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Configuring-API). You can also change username, password and album in`"posting.api"` to the user and album you have if you already have Photos API album and user set up. In that case you don't need to create a new one.
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.
8. Configure posting time:
To make your bot post random content you need to configure `"posting.time"` with a list of "DD:MM" formatted strings or use `"posting.interval"` formatted as "XdXhXmXs". To use interval instead of selected time set `"posting.use_interval"` to `true`.
9. Good to go, run it!
Make sure MongoDB and Photos API are running and use `python 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 `/reboot` command. Additionally there are `loop.sh` and `loop.bat` available if you want your bot to start again after being stopped or after using `/shutdown` command.
If you need any further instructions on how to configure your bot or you had any difficulties doing so - please use [wiki in this repository](https://git.end-play.xyz/profitroll/TelegramPoster/wiki) to get more detailed instructions.
## Command line arguments ## Command line arguments
Of course bot also has them. You can perform some actions with them. Of course bot also has them. You can perform some actions with them.
* `--move-sent` - allows you to move all sent files from queue to sent directories
* `--cleanup` - purge files in both `queue` and `sent` folders if they're sent. Requires `--confirm` argument * `--create-user` - create new API user. Requires config key `"posting.api.address"` to be set;
* `--cleanup-index` - purge all sent entries from index. Requires `--confirm` argument * `--create-album` - create new API album. Requires API address and user config (`"posting.api"`) to be complete.
* `--norun` - allows you to execute above arguments without triggering the bot start itself
Examples: Examples:
* `python3 ./main.py --move-sent --norun`
* `python3 ./main.py --cleanup --confirm` * `python poster.py --create-user`
* `python poster.py --create-user --create-album`
## Tips and improvements
* You may want to configure your bot to work as a systemd service instead. There's [a tutorial for that](https://git.end-play.xyz/profitroll/TelegramPoster/wiki/Configuring-Service) in the wiki.
## Localization ## Localization
Bot is capable of using custom locales. There are some that are pre-installed (English and Ukrainian), however you can add your own locales too. Bot is capable of using custom locales. There are some that are pre-installed (English and Ukrainian), however you can add your own locales too.
All localization files are located in the `locale` folder, otherwise in folder specified in config file. Just copy locale file of your choice, name it in accordance to [IETF language tags](https://en.wikipedia.org/wiki/IETF_language_tag) (if you want your locale to be compatible with Telegram's locales) or define your own name. Save it as json and you're good to go. If you want to change default locale for messages, that cannot determine admin's locale - edit `"locale"` parameter in the `config.json`. If this locale is not available - `"locale_fallback"` will be used instead. If both are not available - error will be shown. For console output and logging locale you should edit `"locale_log"`. All localization files are located in the `locale` folder, otherwise in folder specified in config file. Just copy locale file of your choice, name it in accordance to [IETF language tags](https://en.wikipedia.org/wiki/IETF_language_tag) (if you want your locale to be compatible with Telegram's locales) or define your own name. Save it as json and you're good to go. If you want to change default locale for messages, that cannot determine admin's locale - edit `"locale"` parameter in the `config.json`. If this locale is not available - `"locale_fallback"` will be used instead. If both are not available - error will be shown. For console output and logging locale you should edit `"locale_log"`.
We recommend to only make changes to your custom locale. Or at least always have your backup of for example `en.json` as your fallback. We recommend to only make changes to your custom locale. Or at least always have your backup of for example `en.json` as your fallback.

View File

@@ -1,7 +1,16 @@
# TelegramPoster <h1 align="center">TelegramPoster</h1>
<p align="center">
<a href="https://git.end-play.xyz/profitroll/TelegramPoster/src/branch/master/LICENSE"><img alt="License: GPL" src="https://img.shields.io/badge/License-GPL-blue"></a>
<a href="https://git.end-play.xyz/profitroll/TelegramPoster"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
</p>
## ⚠️ Українська версія README dev гілки ще не готова! Користуйтесь англійською! ⚠️
Цей бот використовується для однієї-єдиної задачі - розміщувати фотографії з мого особистого архіву. Ось його код, тож Ви також можете захостити бота самостійно та розважитися з ним. Тільки не очікуйте, що він ідеальним. Не буде. Але гей, Ви завжди можете його доробити під себе ;) Цей бот використовується для однієї-єдиної задачі - розміщувати фотографії з мого особистого архіву. Ось його код, тож Ви також можете захостити бота самостійно та розважитися з ним. Тільки не очікуйте, що він ідеальним. Не буде. Але гей, Ви завжди можете його доробити під себе ;)
## Установка ## Установка
Для запуску цього бота спочатку потрібно мати інтерпретатор Python та встановлений git. Google — Ваш друг у пошуках. Ви також можете ігнорувати git і просто завантажити код, також має спрацювати добре. Після цього Ви готові до встановлення. Для запуску цього бота спочатку потрібно мати інтерпретатор Python та встановлений git. Google — Ваш друг у пошуках. Ви також можете ігнорувати git і просто завантажити код, також має спрацювати добре. Після цього Ви готові до встановлення.
> У цьому README я вважаю, що Ви використовуєте python за замовчуванням у своїй > У цьому README я вважаю, що Ви використовуєте python за замовчуванням у своїй
@@ -40,19 +49,23 @@
Крім того, доступні `loop.sh` і `loop.bat`, якщо ви хочете, щоб ваш бот запускався знову після зупинки або після використання команди `/reboot`. Крім того, доступні `loop.sh` і `loop.bat`, якщо ви хочете, щоб ваш бот запускався знову після зупинки або після використання команди `/reboot`.
## Аргументи командного рядка ## Аргументи командного рядка
Звичайно, у бота вони також є. З ними можна виконувати деякі дії. Звичайно, у бота вони також є. З ними можна виконувати деякі дії.
* `--move-sent` - дозволяє перемістити всі надіслані файли з черги до папки надісланих * `--move-sent` - дозволяє перемістити всі надіслані файли з черги до папки надісланих
* `--cleanup` - очистити файли в папках `queue` і `sent`, якщо вони вже надіслані. Потрібен аргумент `--confirm` * `--cleanup` - очистити файли в папках `queue` і `sent`, якщо вони вже надіслані. Потрібен аргумент `--confirm`
* `--cleanup-index` - видалити всі надіслані записи з індексу. Потрібен аргумент `--confirm` * `--cleanup-index` - видалити всі надіслані записи з індексу. Потрібен аргумент `--confirm`
* `--norun` - дозволяє виконувати наведені вище аргументи, не запускаючи самого бота * `--norun` - дозволяє виконувати наведені вище аргументи, не запускаючи самого бота
Приклади: Приклади:
* `python3 ./main.py --move-sent --norun` * `python3 ./main.py --move-sent --norun`
* `python3 ./main.py --cleanup --confirm` * `python3 ./main.py --cleanup --confirm`
## Локалізація ## Локалізація
Бот може використовувати різні мови. Є деякі попередньо встановлені (Англійська та Українська), однак Ви можете додавати свої власні локалізації теж. Бот може використовувати різні мови. Є деякі попередньо встановлені (Англійська та Українська), однак Ви можете додавати свої власні локалізації теж.
Всі файли локалізації знаходяться у папці `locale`, якщо в конфігураційному файлі не вказано іншу. Просто скопіюйте цікавлячий Вас файл, назвіть його відповідно до [тегів мови IETF](https://en.wikipedia.org/wiki/IETF_language_tag) (якщо Ви хочете, щоб переклад був сумісним з перекладами Telegram) або просто вкажіть свою власну назву. Збережіть свій переклад як json файл і все готово. Якщо ви хочете змінити мову за замовчуванням для повідомлень самого бота, які не можуть визначити мову адміністратора, відредагуйте параметр `"locale"` у `config.json`. Якщо ця мова недоступна, замість неї буде використано `"locale_fallback"`. Якщо обидві мови недоступні - буде показано помилку. Для зміни мови виведення консолі та логування вам слід відредагувати `"locale_log"`. Всі файли локалізації знаходяться у папці `locale`, якщо в конфігураційному файлі не вказано іншу. Просто скопіюйте цікавлячий Вас файл, назвіть його відповідно до [тегів мови IETF](https://en.wikipedia.org/wiki/IETF_language_tag) (якщо Ви хочете, щоб переклад був сумісним з перекладами Telegram) або просто вкажіть свою власну назву. Збережіть свій переклад як json файл і все готово. Якщо ви хочете змінити мову за замовчуванням для повідомлень самого бота, які не можуть визначити мову адміністратора, відредагуйте параметр `"locale"` у `config.json`. Якщо ця мова недоступна, замість неї буде використано `"locale_fallback"`. Якщо обидві мови недоступні - буде показано помилку. Для зміни мови виведення консолі та логування вам слід відредагувати `"locale_log"`.
Ми рекомендуємо вносити будь-які зміни лише до вашої окремої мови. Або, принаймні, завжди мати резервну копію, наприклад, `en.json` як запасний варіант. Ми рекомендуємо вносити будь-які зміни лише до вашої окремої мови. Або, принаймні, завжди мати резервну копію, наприклад, `en.json` як запасний варіант.

View File

@@ -0,0 +1,8 @@
from enum import Enum
class SubmissionType(Enum):
DOCUMENT = "document"
VIDEO = "video"
ANIMATION = "animation"
PHOTO = "photo"

58
classes/exceptions.py Normal file
View File

@@ -0,0 +1,58 @@
from typing import Any
class SubmissionUnavailableError(Exception):
pass
class SubmissionUploadError(Exception):
def __init__(self, file_path: str, status_code: int, content: Any) -> None:
self.status_code = status_code
self.content = content
super().__init__(
f"Could not upload photo '{file_path}' due to HTTP {self.status_code}: {self.content}"
)
class SubmissionDuplicatesError(Exception):
def __init__(self, file_path: str, duplicates: list) -> None:
self.duplicates = duplicates
super().__init__(
f"Found duplicates of a photo '{file_path}': {self.duplicates}"
)
class 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"])

98
classes/poster_client.py Normal file
View File

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

55
classes/user.py Normal file
View File

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

View File

@@ -1,14 +1,21 @@
{ {
"module": null,
"locale": "en", "locale": "en",
"locale_log": "en", "locale_log": "en",
"locale_fallback": "en", "locale_fallback": "en",
"admin": 0, "owner": 0,
"admins": [],
"bot": { "bot": {
"api_id": 0, "api_id": 0,
"api_hash": "", "api_hash": "",
"bot_token": "" "bot_token": ""
}, },
"database": {
"user": null,
"password": null,
"host": "127.0.0.1",
"port": 27017,
"name": "tgposter"
},
"mode": { "mode": {
"post": true, "post": true,
"submit": true "submit": true
@@ -16,6 +23,7 @@
"reports": { "reports": {
"sent": false, "sent": false,
"error": true, "error": true,
"update": true,
"startup": true, "startup": true,
"shutdown": true "shutdown": true
}, },
@@ -24,18 +32,26 @@
"location": "logs" "location": "logs"
}, },
"locations": { "locations": {
"tmp": "tmp",
"data": "data", "data": "data",
"cache": "cache",
"sent": "data/sent", "sent": "data/sent",
"queue": "data/queue", "queue": "data/queue",
"index": "data/index.json", "index": "data/index.json",
"submit": "data/submit.json",
"blocked": "data/blocked.json",
"locale": "locale" "locale": "locale"
}, },
"posting": { "posting": {
"channel": 0, "channel": 0,
"silent": false, "silent": false,
"move_sent": false, "move_sent": false,
"use_interval": false,
"interval": "1h30m",
"page_size": 300,
"submitted_caption": {
"enabled": true,
"ignore_admins": true,
"text": "#submitted"
},
"extensions": { "extensions": {
"photo": [ "photo": [
"jpg", "jpg",
@@ -60,16 +76,32 @@
"18:00", "18:00",
"20:00", "20:00",
"22:00" "22:00"
] ],
"api": {
"address": "http://localhost:8054",
"address_external": "https://photos.domain.com",
"username": "",
"password": "",
"album": ""
}
}, },
"caption": { "caption": {
"enabled": false, "enabled": false,
"text": "sample text", "link": null,
"link": null "text": [
"sample text"
]
}, },
"submission": { "submission": {
"timeout": 30, "timeout": 30,
"file_size": 15728640, "file_size": 15728640,
"tmp_size": 15728640,
"allow_duplicates": false,
"send_uploaded_id": false,
"require_confirmation": {
"users": true,
"admins": true
},
"mime_types": [ "mime_types": [
"image/png", "image/png",
"image/gif", "image/gif",
@@ -83,6 +115,9 @@
"rules" "rules"
], ],
"commands_admin": [ "commands_admin": [
"reboot" "import",
"export",
"remove",
"shutdown"
] ]
} }

View File

@@ -1 +0,0 @@
[]

View File

@@ -1,5 +0,0 @@
{
"last_id": 0,
"sent": [],
"captions": {}
}

View File

@@ -1 +0,0 @@
{}

1
docs/config_api.md Normal file
View File

@@ -0,0 +1 @@
# Configuring API connection

1
docs/config_channel.md Normal file
View File

@@ -0,0 +1 @@
# Configuring channel and adding your bot

1
docs/config_posts.md Normal file
View File

@@ -0,0 +1 @@
# Configuring posts and duplicates checker

1
docs/config_systemd.md Normal file
View File

@@ -0,0 +1 @@
# Configuring as systemd service

1
docs/config_time.md Normal file
View File

@@ -0,0 +1 @@
# Configuring posting time

1
docs/updating.md Normal file
View File

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

View File

@@ -5,24 +5,62 @@
}, },
"commands_admin": { "commands_admin": {
"forwards": "Check post forwards", "forwards": "Check post forwards",
"reboot": "Restart the bot" "import": "Submit .zip archive with photos",
"export": "Get .zip archive with all photos",
"remove": "Delete photo by its ID",
"purge": "Completely purge bot's queue",
"shutdown": "Turn off the bot"
}, },
"message": { "message": {
"start": "Hi and welcome!\n\nYou can submit your pictures and videos here. We'll review and add them, if we like them. Make sure you send your stuff one at a time and have chosen media that corresponds to our rules.\n\nYou can also write something to us in the description field. We'll send it with the submission itself, if needed.\n\nAlso, make sure you follow the /rules of submission, otherwise your submission will be declined. In case of spam/abuse you may even be blocked.\n\nHave fun and happy submitting!", "start": "Hi and welcome!\n\nYou can submit your pictures and videos here. We'll review and add them, if we like them. Make sure you send your stuff one at a time and have chosen media that corresponds to our rules.\n\nYou can also write something to us in the description field. We'll send it with the submission itself, if needed.\n\nAlso, make sure you follow the /rules of submission, otherwise your submission will be declined. In case of spam/abuse you may even be blocked.\n\nHave fun and happy submitting!",
"rules": "Photos submission rules:\n1. No porn, only erotics and aesthetics\n2. Nipples are semi-allowed, should be either veiled or barely visible\n3. Genitalia strictly prohibited, but labia prints on clothes or nice pubes/panties/butts - are fine", "rules": "Photos submission rules:\n1. No porn, only erotics and aesthetics\n2. Nipples are semi-allowed, should be either veiled or barely visible\n3. Genitalia strictly prohibited, but labia prints on clothes or nice pubes/panties/butts - are fine\n4. Submitting russians is forbidden",
"shutdown": "Shutting down bot with pid `{0}`", "shutdown": "Shutting down bot with pid `{0}`",
"startup": "Starting with pid `{0}`", "startup": "Starting with pid `{0}`",
"startup_downtime_minutes": "Starting with pid `{0}` (was down for {1} m.)",
"startup_downtime_hours": "Starting with pid `{0}` (was down for {1} h.)",
"startup_downtime_days": "Starting with pid `{0}` (was down for {1} d.)",
"sub_yes": "✅ Submission approved and accepted", "sub_yes": "✅ Submission approved and accepted",
"sub_yes_auto": "✅ Submission automatically accepted",
"sub_no": "❌ Submission reviewed and declined", "sub_no": "❌ Submission reviewed and declined",
"sub_dup": "⚠️ Submission automatically declined because database already contains this photo",
"sub_blocked": "You were blocked and you can't submit media anymore.", "sub_blocked": "You were blocked and you can't submit media anymore.",
"sub_unblocked": "You were unblocked and you can now submit media.", "sub_unblocked": "You were unblocked and you can now submit media.",
"sub_by": "\n\nSubmitted by:", "sub_by": "\n\nSubmitted by:",
"sub_sent": "Media has been submitted.\nWe'll notify you whether it will be accepted or not soon.", "sub_sent": "Media has been submitted.\nWe'll notify you whether it will be accepted or not soon.",
"sub_cooldown": "You can only submit 1 media per {0} seconds", "sub_cooldown": "You can only submit 1 media per {0} seconds",
"sub_media_failed": "Could not upload submission to API. Please check logs for details.",
"sub_media_duplicates": "⚠️ Image duplicates found",
"sub_media_duplicates_list": "It seems like following image has duplicates in API's Db.\n\nNext files marked as similar:\n • {0}",
"document_too_large": "File you've sent is too large. Please submit files not bigger than {0} MB", "document_too_large": "File you've sent is too large. Please submit files not bigger than {0} MB",
"mime_not_allowed": "File type not allowed. Please, consider using one of these: {0}", "mime_not_allowed": "File type not allowed. Please, consider using one of these: {0}",
"post_exception": "Could not send content due to `{exp}`\n\nTraceback:\n```{0}```", "post_exception": "Could not send content due to `{0}`\n\nTraceback:\n```{1}```",
"post_empty": "Could not send content: `Queue folder is empty or contains only unsupported or already sent files.`" "post_invalid_pic": "⚠️ Error {0} while sending photo\n```python\n{1}\n```",
"api_queue_empty": "Could not send content: `Queue is empty or contains only unsupported files.`",
"api_queue_error": "Could not get photo from API's queue. Check the log above or API's errors to get more info.",
"post_low": "Low amount of content: `There are only {0} files left in the queue.`",
"api_creds_invalid": "Could not authorize API access. Please check whether provided in config file are valid and update them if they're not.",
"sub_wip": "Post submission is now WIP. It will be available again in a few days. Thank you for your patience.",
"sub_error": "⚠️ Could not upload this image due to bot error. Admins are advised.",
"sub_error_admin": "User {0} could not submit photo without additional confirmation due to:\n```\n{1}\n```",
"import_request": "Please send me a zip archive with your media to be imported. Use /cancel if you want to abort this operation.",
"import_ignored": "No response, aborting import.",
"import_abort": "Import aborted.",
"import_invalid_media": "File to import must be a zip archive. Aborting.",
"import_invalid_mime": "Provided file is not supported. Please send `application/zip`. Aborting.",
"import_too_big": "You archive is `{0} GiB` big, but system has only `{1} GiB` free. Unpacking may take even more space. Aborting.",
"import_downloading": "Downloading archive...",
"import_unpacking": "Unpacking archive...",
"import_unpack_error": "Could not unpack the archive\n\nException: {0}\n\nTraceback:\n```python\n{1}\n```",
"import_uploading": "Uploading archive contents...",
"import_upload_error_duplicate": "Could not upload `{0}` because there're duplicates on server.",
"import_upload_error_other": "Could not upload `{0}`. Probably disallowed filetype.",
"import_finished": "Import finished.",
"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",
@@ -41,16 +79,19 @@
"sub_unblock": "User {0} has been unblocked", "sub_unblock": "User {0} has been unblocked",
"sub_msg_unavail": "Submission message no longer exist", "sub_msg_unavail": "Submission message no longer exist",
"sub_media_unavail": "Could not download submission", "sub_media_unavail": "Could not download submission",
"sub_done": "You've already decided what to do with submission" "sub_done": "You've already decided what to do with submission",
"sub_duplicates_found": "There're duplicates in bot's database",
"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} of type {1} to {2} with caption {3} and silently {4}", "post_sent": "Sent {0} to {1} with caption {2} and silently {3}",
"post_exception": "Could not send content due to {0}. Traceback: {1}", "post_exception": "Could not send content due to {0}. Traceback: {1}",
"post_empty": "Could not send content due to queue folder empty with allowed extensions", "post_invalid_pic": "Error while sending photo HTTP {0}: {1}",
"post_empty": "Could not send content due to queue empty or contains only forbidden extensions",
"sub_mime_not_allowed": "Got submission from {0} but type of {1} which is not allowed", "sub_mime_not_allowed": "Got submission from {0} but type of {1} which is not allowed",
"sub_document_too_large": "Got submission from {0} but but file is too large ({1} > {2})", "sub_document_too_large": "Got submission from {0} but but file is too large ({1} > {2})",
"sub_received": "Got submission from {0} with a caption {1}", "sub_received": "Got submission from {0} with a caption {1}",
@@ -73,6 +114,17 @@
"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

@@ -5,24 +5,62 @@
}, },
"commands_admin": { "commands_admin": {
"forwards": "Переглянути репости", "forwards": "Переглянути репости",
"reboot": "Перезапустити бота" "import": "Надати боту .zip архів з фотографіями",
"export": "Отримати .zip архів з усіма фотографіями",
"remove": "Видалити фото за його ID",
"purge": "Повністю видалити всю чергу бота",
"shutdown": "Вимкнути бота"
}, },
"message": { "message": {
"start": "Привіт і ласкаво просимо!\n\nТут можна пропонувати свої фотографії та відео. Ми переглянемо та додамо їх, якщо вони нам сподобаються. Переконайтеся, що ви надсилаєте свої матеріали по одному та вибираєте медіа, які відповідають нашим правилам.\n\nВи також можете написати нам щось у полі опису. За потреби ми надішлемо це разом із самим фото.\n\nКрім того, переконайтеся, що ви дотримуєтеся /rules (правил) подання, інакше вашу пропозицію буде відхилено. У разі спаму/зловживань вас можуть навіть заблокувати.\n\nГарного дня та щасливого надсилання!", "start": "Привіт і ласкаво просимо!\n\nТут можна пропонувати свої фотографії та відео. Ми переглянемо та додамо їх, якщо вони нам сподобаються. Переконайтеся, що ви надсилаєте свої матеріали по одному та вибираєте медіа, які відповідають нашим правилам.\n\nВи також можете написати нам щось у полі опису. За потреби ми надішлемо це разом із самим фото.\n\nКрім того, переконайтеся, що ви дотримуєтеся /rules (правил) подання, інакше вашу пропозицію буде відхилено. У разі спаму/зловживань вас можуть навіть заблокувати.\n\nГарного дня та щасливого надсилання!",
"rules": "Правила пропонування фото:\n1. Ніякого порно, тільки еротика та естетика\n2. Соски можна, але або завуальовані, або зовсім ледь помітні\n3. Геніталії суворо ні, а ось відбитки статевих губ на одязі або гарні лобочки/трусики/попки - без проблем", "rules": "Правила пропонування фото:\n1. Ніякого порно, тільки еротика та естетика\n2. Соски можна, але або завуальовані, або зовсім ледь помітні\n3. Геніталії суворо ні, а ось відбитки статевих губ на одязі або гарні лобочки/трусики/попки - без проблем\n4. Пропонувати русню заборонено",
"shutdown": "Вимкнення бота з підом `{0}`", "shutdown": "Вимкнення бота з підом `{0}`",
"startup": "Запуск бота з підом `{0}`", "startup": "Запуск бота з підом `{0}`",
"startup_downtime_minutes": "Запуск бота з підом `{0}` (лежав {1} хв.)",
"startup_downtime_hours": "Запуск бота з підом `{0}` (лежав {1} год.)",
"startup_downtime_days": "Запуск бота з підом `{0}` (лежав {1} дн.)",
"sub_yes": "✅ Подання схвалено та прийнято", "sub_yes": "✅ Подання схвалено та прийнято",
"sub_yes_auto": "✅ Подання автоматично прийнято",
"sub_no": "❌ Подання розглянуто та відхилено", "sub_no": "❌ Подання розглянуто та відхилено",
"sub_dup": "⚠️ Подання автоматично відхилено через наявність цього фото в базі даних",
"sub_blocked": "Вас заблокували, ви більше не можете надсилати медіафайли.", "sub_blocked": "Вас заблокували, ви більше не можете надсилати медіафайли.",
"sub_unblocked": "Вас розблокували, тепер ви можете надсилати медіафайли.", "sub_unblocked": "Вас розблокували, тепер ви можете надсилати медіафайли.",
"sub_by": "\n\nПредставлено:", "sub_by": "\n\nПредставлено:",
"sub_sent": "Медіа-файл надіслано.\nСкоро ми повідомимо вас, чи буде його прийнято.", "sub_sent": "Медіа-файл надіслано.\nСкоро ми повідомимо вас, чи буде його прийнято.",
"sub_cooldown": "Ви можете надсилати лише 1 медіафайл на {0} секунд", "sub_cooldown": "Ви можете надсилати лише 1 медіафайл на {0} секунд",
"sub_media_failed": "Не вдалося завантажити подання на сервер. Перевірте логи для деталей.",
"sub_media_duplicates": "⚠️ Знайдено зображення-дублікати",
"sub_media_duplicates_list": "Здається, подане зображення має дублікати в базі даних.\n\nНаступні файли було відмічено як дуже схожі з поданням:\n • {0}",
"document_too_large": "Надісланий файл завеликий. Будь ласка, надсилайте файли не більше {0} Мб", "document_too_large": "Надісланий файл завеликий. Будь ласка, надсилайте файли не більше {0} Мб",
"mime_not_allowed": "Тип файлу не дозволений. Розгляньте можливість використання одного з цих: {0}", "mime_not_allowed": "Тип файлу не дозволений. Розгляньте можливість використання одного з цих: {0}",
"post_exception": "Не вдалося надіслати контент через `{exp}`\n\nTraceback:\n```{0}```", "post_exception": "Не вдалося надіслати контент через `{0}`\n\nTraceback:\n```{1}```",
"post_empty": "Не вдалося надіслати контент: «Папка черги порожня або містить лише непідтримувані або вже надіслані файли»." "post_invalid_pic": "⚠️ Помилка надсилання фото {0}\n```python\n{1}\n```",
"api_queue_empty": "Не вдалося надіслати контент: `Черга порожня або містить лише непідтримувані файли`.",
"api_queue_error": "Не вдалось отримати фото з черги API. Погляньте на логи вище а також на лог помилок API щоб дізнатись подробиці.",
"post_low": "Мала кількість контенту: `Залишилось всього {0} файлів в черзі.`",
"api_creds_invalid": "Не вдалося авторизувати запит до API. Будь ласка, перевірте чи дані авторизації в конфігураційному файлі вірні та оновіть їх, якщо це не так.",
"sub_wip": "Подання постів зараз знаходиться у розробці. Він буде знову доступний через кілька днів. Дякуємо за ваше терпіння.",
"sub_error": "⚠️ Не вдалось завантажити фото через помилку бота. Адміністрацію повідомлено.",
"sub_error_admin": "Користувач {0} не зміг надіслати фото без додаткової перевірки через помилку:\n```\n{1}\n```",
"import_request": "Будь ласка, надішліть zip-архів з медіа для імпортування. Використовуйте /cancel, якщо ви хочете перервати цю операцію.",
"import_ignored": "Немає відповіді, перериваємо імпорт.",
"import_abort": "Імпорт перервано.",
"import_invalid_media": "Файл для імпорту має бути zip-архівом. Перериваємо.",
"import_invalid_mime": "Наданий файл не підтримується. Будь ласка, надішліть `application/zip`. Перервано.",
"import_too_big": "Ваш архів має розмір `{0} GiB`, але система має лише `{1} GiB` вільних. Розпакування може зайняти значно більше місця. Перервано.",
"import_downloading": "Завантажуємо архів...",
"import_unpacking": "Розпаковуємо архів...",
"import_unpack_error": "Не вдалося розпакувати архів\n\nПомилка: {0}\n\nTraceback:\n```python\n{1}\n```",
"import_uploading": "Завантажуємо вміст архіву...",
"import_upload_error_duplicate": "Не вдалося завантажити `{0}`, оскільки на сервері є дублікати.",
"import_upload_error_other": "Не вдалося завантажити `{0}`. Ймовірно, заборонений тип файлу.",
"import_finished": "Імпорт завершено.",
"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": "✅ Прийняти",
@@ -41,16 +79,19 @@
"sub_unblock": "Користувача {0} розблоковано", "sub_unblock": "Користувача {0} розблоковано",
"sub_msg_unavail": "Повідомлення більше не існує", "sub_msg_unavail": "Повідомлення більше не існує",
"sub_media_unavail": "Не вдалося завантажити подання", "sub_media_unavail": "Не вдалося завантажити подання",
"sub_done": "Ви вже обрали що зробити з цим поданням" "sub_done": "Ви вже обрали що зробити з цим поданням",
"sub_duplicates_found": "Знайдено дублікати в базі даних бота",
"nothing": "🏁 Цю дію вже було завершено"
}, },
"console": { "console": {
"shutdown": "Вимкнення бота з підом {0}", "shutdown": "Вимкнення бота з підом {0}",
"startup": "Запуск бота з підом {0}", "startup": "Запуск бота з підом {0}",
"keyboard_interrupt": "\nВимикаюсь...", "keyboard_interrupt": "\nВимикаюсь...",
"exception_occured": "Помилка {0} сталась під час виконання", "exception_occured": "Помилка {0} сталась під час виконання",
"post_sent": "Надіслано {0} типу {1} у {2} з підписом {3} та без звуку {4}", "post_sent": "Надіслано {0} у {1} з підписом {2} та без звуку {3}",
"post_exception": "Не вдалося надіслати контент через {0}. Traceback: {1}", "post_exception": "Не вдалося надіслати контент через {0}. Traceback: {1}",
"post_empty": "Не вдалося надіслати контент через порожню папку черги з дозволеними розширеннями", "post_invalid_pic": "Помилка надсилання фото HTTP {0}: {1}",
"post_empty": "Не вдалося надіслати контент адже черга з дозволеними розширеннями порожня",
"sub_mime_not_allowed": "Отримано подання від {0} але типу {1} який не є дозволеним", "sub_mime_not_allowed": "Отримано подання від {0} але типу {1} який не є дозволеним",
"sub_document_too_large": "Отримано подання від {0} але файл завеликий({1} > {2})", "sub_document_too_large": "Отримано подання від {0} але файл завеликий({1} > {2})",
"sub_received": "Отримано подання від {0} з підписом {1}", "sub_received": "Отримано подання від {0} з підписом {1}",
@@ -73,6 +114,17 @@
"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}"
} }
} }

263
modules/api_client.py Normal file
View File

@@ -0,0 +1,263 @@
"""This is only a temporary solution. Complete Photos API client is yet to be developed."""
import asyncio
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
import aiofiles
from aiohttp import FormData
from classes.exceptions import (
AlbumCreationDuplicateError,
AlbumCreationError,
AlbumCreationNameError,
SubmissionUploadError,
UserCreationDuplicateError,
UserCreationError,
)
from modules.logger import logWrite
from modules.utils import configGet, locale
from modules.http_client import http_session
async def authorize() -> str:
makedirs(configGet("cache", "locations"), exist_ok=True)
if path.exists(configGet("cache", "locations") + sep + "api_access") is True:
async with aiofiles.open(
configGet("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/",
headers={"Authorization": f"Bearer {token}"},
)
).status == 200:
return token
payload = {
"grant_type": "password",
"scope": "me albums.list albums.read albums.write photos.list photos.read photos.write videos.list videos.read videos.write",
"username": configGet("username", "posting", "api"),
"password": configGet("password", "posting", "api"),
}
response = await http_session.post(
configGet("address", "posting", "api") + "/token", data=payload
)
if not response.ok:
logWrite(
locale(
"api_creds_invalid",
"console",
locale=configGet("locale_log").format(
configGet("address", "posting", "api"),
configGet("username", "posting", "api"),
response.status,
),
)
)
raise ValueError
async with aiofiles.open(
configGet("cache", "locations") + sep + "api_access", "wb"
) as file:
await file.write(
b64encode((await response.json())["access_token"].encode("utf-8"))
)
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.
### 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'
)
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
if __name__ == "__main__":
print(asyncio.run(authorize()))

14
modules/app.py Normal file
View File

@@ -0,0 +1,14 @@
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 = []

78
modules/cli.py Normal file
View File

@@ -0,0 +1,78 @@
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,22 +0,0 @@
RESET = '\u001b[0m'
BLACK = '\u001b[30m'
RED = '\u001b[31m'
GREEN = '\u001b[32m'
YELLOW = '\u001b[33m'
BLUE = '\u001b[34m'
MAGENTA = '\u001b[35m'
CYAN = '\u001b[36m'
WHITE = '\u001b[37m'
BBLACK = '\u001b[30;1m'
BRED = '\u001b[31;1m'
BGREEN = '\u001b[32;1m'
BYELLOW = '\u001b[33;1m'
BBLUE = '\u001b[34;1m'
BMAGENTA = '\u001b[35;1m'
BCYAN = '\u001b[36;1m'
BWHITE = '\u001b[37;1m'
ULINE = '\u001b[4m'
REVERSE = '\u001b[7m'

View File

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

35
modules/database.py Normal file
View File

@@ -0,0 +1,35 @@
"""Module that provides all database columns"""
from pymongo import MongoClient
from ujson import loads
with open("config.json", "r", encoding="utf-8") as f:
db_config = loads(f.read())["database"]
f.close()
if db_config["user"] is not None and db_config["password"] is not None:
con_string = "mongodb://{0}:{1}@{2}:{3}/{4}".format(
db_config["user"],
db_config["password"],
db_config["host"],
db_config["port"],
db_config["name"],
)
else:
con_string = "mongodb://{0}:{1}/{2}".format(
db_config["host"], db_config["port"], db_config["name"]
)
db_client = MongoClient(con_string)
db = db_client.get_database(name=db_config["name"])
collections = db.list_collection_names()
for collection in ["sent", "users", "banned", "submitted"]:
if not collection in collections:
db.create_collection(collection)
col_sent = db.get_collection("sent")
col_users = db.get_collection("users")
col_banned = db.get_collection("banned")
col_submitted = db.get_collection("submitted")

6
modules/http_client.py Normal file
View File

@@ -0,0 +1,6 @@
from aiohttp import ClientSession
from ujson import dumps
http_session = ClientSession(
json_serialize=dumps,
)

67
modules/logger.py Normal file
View File

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

31
modules/scheduler.py Normal file
View File

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

201
modules/sender.py Normal file
View File

@@ -0,0 +1,201 @@
from datetime import datetime
from os import makedirs, path
from random import choice
from shutil import rmtree
from traceback import format_exc
from uuid import uuid4
from PIL import Image
import aiofiles
from classes.poster_client import PosterClient
from modules.api_client import authorize, move_pic, random_pic, http_session
from modules.database import col_sent, col_submitted
from modules.logger import logWrite
from modules.utils import configGet, locale
async def send_content(app: PosterClient) -> None:
try:
try:
token = await authorize()
except ValueError:
await app.send_message(
app.owner,
locale("api_creds_invalid", "message", locale=configGet("locale")),
)
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
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()))
)
if configGet("error", "reports"):
await app.send_message(
app.owner,
locale(
"post_invalid_pic", "message", locale=configGet("locale")
).format(response.status, response.json()),
)
tmp_dir = str(uuid4())
makedirs(path.join(configGet("tmp", "locations"), tmp_dir), exist_ok=True)
tmp_path = path.join(tmp_dir, pic[1])
async with aiofiles.open(
path.join(configGet("tmp", "locations"), tmp_path), "wb"
) as out_file:
await out_file.write(await response.read())
logWrite(
f'Candidate {pic[1]} ({pic[0]}) is {path.getsize(path.join(configGet("tmp", "locations"), tmp_path))} bytes big',
debug=True,
)
if path.getsize(path.join(configGet("tmp", "locations"), tmp_path)) > 5242880:
image = Image.open(path.join(configGet("tmp", "locations"), 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),
"JPEG",
optimize=True,
quality=50,
)
elif tmp_path.lower().endswith(".png"):
image.save(
path.join(configGet("tmp", "locations"), tmp_path),
"PNG",
optimize=True,
compress_level=8,
)
image.close()
if path.getsize(path.join(configGet("tmp", "locations"), tmp_path)) > 5242880:
rmtree(
path.join(configGet("tmp", "locations"), tmp_dir), ignore_errors=True
)
raise BytesWarning
del response
submitted = col_submitted.find_one({"temp.file": pic[1]})
if submitted is not None and submitted["caption"] is not None:
caption = submitted["caption"].strip()
else:
caption = ""
if (
submitted is not None
and configGet("enabled", "posting", "submitted_caption")
and (
(submitted["user"] not in app.admins)
or (configGet("ignore_admins", "posting", "submitted_caption") is False)
)
):
caption = (
f"{caption}\n\n{configGet('text', 'posting', 'submitted_caption')}\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')})"
else:
caption = f"{caption}{choice(configGet('text', 'caption'))}"
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"),
)
except Exception as exp:
logWrite(f"Could not send image {pic[1]} ({pic[0]}) due to {exp}")
if configGet("error", "reports"):
await app.send_message(
app.owner,
locale(
"post_exception", "message", locale=configGet("locale")
).format(exp, format_exc()),
)
# rmtree(path.join(configGet("tmp", "locations"), tmp_dir), ignore_errors=True)
return
col_sent.insert_one(
{
"date": datetime.now(),
"image": pic[0],
"filename": pic[1],
"channel": configGet("channel", "posting"),
"caption": None
if (submitted is None or submitted["caption"] is None)
else submitted["caption"].strip(),
}
)
await move_pic(pic[0])
rmtree(path.join(configGet("tmp", "locations"), tmp_dir), ignore_errors=True)
logWrite(
locale("post_sent", "console", locale=configGet("locale")).format(
pic[0],
str(configGet("channel", "posting")),
caption.replace("\n", "%n"),
str(configGet("silent", "posting")),
)
)
except Exception as exp:
logWrite(
locale("post_exception", "console", locale=configGet("locale")).format(
str(exp), format_exc()
)
)
if configGet("error", "reports"):
await app.send_message(
app.owner,
locale("post_exception", "message", locale=configGet("locale")).format(
exp, format_exc()
),
)
try:
rmtree(
path.join(configGet("tmp", "locations"), tmp_dir), ignore_errors=True
)
except:
pass

View File

@@ -1,39 +1,128 @@
try: from os import kill, makedirs
from ujson import JSONDecodeError as JSONDecodeError from os import name as osname
from ujson import loads, dumps from os import path, sep
except ModuleNotFoundError: from sys import exit
from json import JSONDecodeError as JSONDecodeError from traceback import print_exc
from json import loads, dumps from typing import Any
from zipfile import ZipFile
import os import aiofiles
import sys from ujson import JSONDecodeError, dumps, loads
import traceback
from signal import SIGKILL # type: ignore from modules.logger import logWrite
from modules.logging import logWrite
def jsonLoad(filename): 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:
"""Loads arg1 as json and returns its contents""" """Loads arg1 as json and returns its contents"""
with open(filename, "r", encoding='utf8') as file: with open(filename, "r", encoding="utf8") as file:
try: try:
output = loads(file.read()) output = loads(file.read())
except JSONDecodeError: except JSONDecodeError:
logWrite(f"Could not load json file {filename}: file seems to be incorrect!\n{traceback.print_exc()}") logWrite(
f"Could not load json file {filename}: file seems to be incorrect!\n{print_exc()}"
)
raise raise
except FileNotFoundError: except FileNotFoundError:
logWrite(f"Could not load json file {filename}: file does not seem to exist!\n{traceback.print_exc()}") logWrite(
f"Could not load json file {filename}: file does not seem to exist!\n{print_exc()}"
)
raise raise
file.close() file.close()
return output return output
def jsonSave(contents, filename):
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(dumps(contents, ensure_ascii=False, indent=4)) file.write(
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{traceback.print_exc()}") logWrite(f"Could not save json file {filename}: {exp}\n{print_exc()}")
return return
@@ -43,7 +132,7 @@ def configSet(key: str, value, *args: str):
* key (str): The last key of the keys path. * key (str): The last key of the keys path.
* value (str/int/float/list/dict/None): Some needed value. * value (str/int/float/list/dict/None): Some needed value.
* *args (str): Path to key like: dict[args][key]. * *args (str): Path to key like: dict[args][key].
""" """
this_dict = jsonLoad("config.json") this_dict = jsonLoad("config.json")
string = "this_dict" string = "this_dict"
for arg in args: for arg in args:
@@ -56,6 +145,7 @@ def configSet(key: str, value, *args: str):
jsonSave(this_dict, "config.json") jsonSave(this_dict, "config.json")
return return
def configGet(key: str, *args: str): def configGet(key: str, *args: str):
"""Get value of the config key """Get value of the config key
Args: Args:
@@ -63,13 +153,25 @@ def configGet(key: str, *args: str):
* *args (str): Path to key like: dict[args][key]. * *args (str): Path to key like: dict[args][key].
Returns: Returns:
* any: Value of provided key * any: Value of provided key
""" """
this_dict = jsonLoad("config.json") this_dict = jsonLoad("config.json")
this_key = this_dict try:
for dict_key in args: this_key = this_dict
this_key = this_key[dict_key] 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] return this_key[key]
def locale(key: str, *args: str, locale=configGet("locale")): def locale(key: str, *args: str, locale=configGet("locale")):
"""Get value of locale string """Get value of locale string
Args: Args:
@@ -78,39 +180,73 @@ def locale(key: str, *args: str, locale=configGet("locale")):
* locale (str): Locale to looked up in. Defaults to config's locale value. * locale (str): Locale to looked up in. Defaults to config's locale value.
Returns: Returns:
* any: Value of provided locale key * any: Value of provided locale key
""" """
if (locale == None): if locale == None:
locale = configGet("locale") locale = configGet("locale")
try: try:
this_dict = jsonLoad(f'{configGet("locale", "locations")}{os.sep}{locale}.json') this_dict = jsonLoad(f'{configGet("locale", "locations")}{sep}{locale}.json')
except FileNotFoundError: except FileNotFoundError:
try: try:
this_dict = jsonLoad(f'{configGet("locale", "locations")}{os.sep}{configGet("locale")}.json') this_dict = jsonLoad(
f'{configGet("locale", "locations")}{sep}{configGet("locale")}.json'
)
except FileNotFoundError: except FileNotFoundError:
try: try:
this_dict = jsonLoad(f'{configGet("locale_fallback", "locations")}{os.sep}{configGet("locale")}.json') this_dict = jsonLoad(
f'{configGet("locale_fallback", "locations")}{sep}{configGet("locale")}.json'
)
except: except:
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}"'
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]
try: try:
return this_key[key] return this_key[key]
except KeyError: except KeyError:
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:
import psutil from psutil import Process
except ModuleNotFoundError: except ModuleNotFoundError:
print(locale("deps_missing", "console", locale=configGet("locale")), flush=True) print(locale("deps_missing", "console", locale=configGet("locale")), flush=True)
sys.exit() exit()
def killProc(pid):
if os.name == "posix": def killProc(pid: int) -> None:
os.kill(pid, SIGKILL) """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: else:
p = psutil.Process(pid) Process(pid).kill()
p.kill()

View File

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

View File

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

@@ -0,0 +1,274 @@
from os 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
@app.on_callback_query(filters.regex("sub_yes_[\s\S]*"))
async def callback_query_yes(app: PosterClient, clb: CallbackQuery):
fullclb = str(clb.data).split("_")
user_locale = clb.from_user.language_code
db_entry = col_submitted.find_one({"_id": ObjectId(fullclb[2])})
try:
submission = await app.submit_photo(fullclb[2])
except SubmissionUnavailableError:
await clb.answer(
text=locale("sub_msg_unavail", "callback", locale=user_locale),
show_alert=True,
)
return
except SubmissionDuplicatesError as exp:
await clb.answer(
text=locale("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(
"\n".join(exp.duplicates)
),
quote=True,
)
logWrite(
locale(
"submission_duplicate",
"console",
locale=configGet("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),
quote=True,
)
elif db_entry is not None:
await app.send_message(db_entry["user"], locale("sub_yes", "message"))
await clb.answer(
text=locale("sub_yes", "callback", locale=user_locale).format(fullclb[2]),
show_alert=True,
)
edited_markup = (
[
[
InlineKeyboardButton(
text=str(locale("accepted", "button", locale=user_locale)),
callback_data="nothing",
)
],
clb.message.reply_markup.inline_keyboard[1],
]
if len(clb.message.reply_markup.inline_keyboard) > 1
else [
[
InlineKeyboardButton(
text=str(locale("accepted", "button", locale=user_locale)),
callback_data="nothing",
)
]
]
)
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(
reply_markup=InlineKeyboardMarkup(edited_markup)
)
logWrite(
locale(
"submission_accepted",
"console",
locale=configGet("locale_log").format(fullclb[2], submission[1]),
),
debug=True,
)
# 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):
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,
)
try:
submission = await app.get_messages(
db_entry["user"], db_entry["telegram"]["msg_id"]
)
except:
await clb.answer(
text=locale("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),
quote=True,
)
await clb.answer(
text=locale("sub_no", "callback", locale=user_locale).format(fullclb[2]),
show_alert=True,
)
edited_markup = (
[
[
InlineKeyboardButton(
text=str(locale("declined", "button", locale=user_locale)),
callback_data="nothing",
)
],
clb.message.reply_markup.inline_keyboard[1],
]
if len(clb.message.reply_markup.inline_keyboard) > 1
else [
[
InlineKeyboardButton(
text=str(locale("declined", "button", locale=user_locale)),
callback_data="nothing",
)
]
]
)
await clb.message.edit_reply_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]*"))
async def callback_query_block(app: PosterClient, 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"))
)
PosterUser(int(fullclb[2])).block()
await clb.answer(
text=locale("sub_block", "callback", locale=user_locale).format(fullclb[2]),
show_alert=True,
)
edited_markup = [
clb.message.reply_markup.inline_keyboard[0],
[
InlineKeyboardButton(
text=str(locale("sub_unblock", "button", locale=user_locale)),
callback_data=f"sub_unblock_{fullclb[2]}",
)
],
]
await clb.message.edit_reply_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]*"))
async def callback_query_unblock(app: PosterClient, 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"))
)
PosterUser(int(fullclb[2])).unblock()
await clb.answer(
text=locale("sub_unblock", "callback", locale=user_locale).format(fullclb[2]),
show_alert=True,
)
edited_markup = [
clb.message.reply_markup.inline_keyboard[0],
[
InlineKeyboardButton(
text=str(locale("sub_block", "button", locale=user_locale)),
callback_data=f"sub_block_{fullclb[2]}",
)
],
]
await clb.message.edit_reply_markup(
reply_markup=InlineKeyboardMarkup(edited_markup)
)
logWrite(
locale(
"user_unblocked",
"console",
locale=configGet("locale_log").format(fullclb[2]),
),
debug=True,
)

View File

@@ -0,0 +1,45 @@
from os import getpid, makedirs, path
from time import time
from pyrogram import filters
from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
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
@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}")
await msg.reply_text(
locale("shutdown", "message", locale=msg.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

@@ -0,0 +1,23 @@
from pyrogram import filters
from pyrogram.types import Message
from modules.app import app
from modules.utils import locale
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)
)
@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)
)

214
plugins/commands/photos.py Normal file
View File

@@ -0,0 +1,214 @@
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.types import Message
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
@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
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=["", "/"]))
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)
)
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=["", "/"]))
async def cmd_purge(app: PosterClient, msg: Message):
if msg.from_user.id in app.admins:
pass

View File

@@ -0,0 +1,287 @@
from datetime import datetime
from os import makedirs, path, sep
from traceback import format_exc
from uuid import uuid4
from pyrogram import filters
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.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
@app.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:
return
try:
if col_banned.find_one({"user": msg.from_user.id}) is not None:
return
await app.send_chat_action(msg.chat.id, ChatAction.TYPING)
user_locale = msg.from_user.language_code
save_tmp = True
contents = None
if PosterUser(msg.from_user.id).is_limited():
await msg.reply_text(
locale("sub_cooldown", "message", locale=user_locale).format(
str(configGet("timeout", "submission"))
)
)
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,
)
if msg.document.mime_type not in configGet("mime_types", "submission"):
await msg.reply_text(
locale("mime_not_allowed", "message", locale=user_locale).format(
", ".join(configGet("mime_types", "submission"))
),
quote=True,
)
return
if msg.document.file_size > configGet("file_size", "submission"):
await msg.reply_text(
locale("document_too_large", "message", locale=user_locale).format(
str(configGet("file_size", "submission") / 1024 / 1024)
),
quote=True,
)
return
if msg.document.file_size > configGet("tmp_size", "submission"):
save_tmp = False
contents = (
msg.document.file_id,
SubmissionType.DOCUMENT,
) # , 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,
)
if msg.video.file_size > configGet("file_size", "submission"):
await msg.reply_text(
locale("document_too_large", "message", locale=user_locale).format(
str(configGet("file_size", "submission") / 1024 / 1024)
),
quote=True,
)
return
if msg.video.file_size > configGet("tmp_size", "submission"):
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,
)
if msg.animation.file_size > configGet("file_size", "submission"):
await msg.reply_text(
locale("document_too_large", "message", locale=user_locale).format(
str(configGet("file_size", "submission") / 1024 / 1024)
),
quote=True,
)
return
if msg.animation.file_size > configGet("tmp_size", "submission"):
save_tmp = False
contents = (
msg.animation.file_id,
SubmissionType.ANIMATION,
) # , 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,
)
contents = msg.photo.file_id, SubmissionType.PHOTO # , "please_generate"
if save_tmp is not None:
if contents is None:
return
tmp_id = str(uuid4())
# filename = tmp_id if contents[1] == "please_generate" else contents[1]
makedirs(
path.join(configGet("data", "locations"), "submissions", tmp_id),
exist_ok=True,
)
downloaded = await app.download_media(
msg,
path.join(configGet("data", "locations"), "submissions", tmp_id) + sep,
)
inserted = col_submitted.insert_one(
{
"user": msg.from_user.id,
"date": datetime.now(),
"done": False,
"type": contents[1].value,
"temp": {"uuid": tmp_id, "file": path.basename(str(downloaded))},
"telegram": {"msg_id": msg.id, "file_id": contents[0]},
"caption": str(msg.caption) if msg.caption is not None else None,
}
)
else:
if contents is None:
return
inserted = col_submitted.insert_one(
{
"user": msg.from_user.id,
"date": datetime.now(),
"done": False,
"type": contents[1].value,
"temp": {"uuid": None, "file": None},
"telegram": {"msg_id": msg.id, "file_id": contents[0]},
"caption": str(msg.caption) if msg.caption is not None else None,
}
)
buttons = [
[
InlineKeyboardButton(
text=locale("sub_yes", "button", locale=configGet("locale")),
callback_data=f"sub_yes_{str(inserted.inserted_id)}",
)
]
]
if msg.caption is not None:
caption = str(msg.caption)
buttons[0].append(
InlineKeyboardButton(
text=locale(
"sub_yes_caption", "button", locale=configGet("locale")
),
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")))
if msg.from_user.first_name is not None:
caption += f" {msg.from_user.first_name}"
if msg.from_user.last_name is not None:
caption += f" {msg.from_user.last_name}"
if msg.from_user.username is not None:
caption += f" (@{msg.from_user.username})"
if msg.from_user.phone_number is not None:
caption += f" ({msg.from_user.phone_number})"
if (
msg.from_user.id in app.admins
and configGet("admins", "submission", "require_confirmation") is False
):
try:
submitted = await app.submit_photo(str(inserted.inserted_id))
await msg.reply_text(
locale("sub_yes_auto", "message", locale=user_locale),
disable_notification=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)
return
except SubmissionDuplicatesError as exp:
await msg.reply_text(
locale(
"sub_media_duplicates_list", "message", locale=user_locale
).format("\n".join(exp.duplicates)),
quote=True,
)
return
except Exception as exp:
await msg.reply_text(format_exc(), quote=True)
return
elif (
msg.from_user.id not in app.admins
and configGet("users", "submission", "require_confirmation") is False
):
try:
submitted = await app.submit_photo(str(inserted.inserted_id))
await msg.reply_text(
locale("sub_yes_auto", "message", locale=user_locale),
disable_notification=True,
quote=True,
)
if configGet("send_uploaded_id", "submission"):
caption += f"\n\nID: `{submitted[1]}`"
await msg.copy(app.owner, caption=caption)
return
except SubmissionDuplicatesError as exp:
await msg.reply_text(
locale("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(
msg.from_user.id, format_exc()
),
)
await msg.reply_text("sub_error", quote=True)
return
if msg.from_user.id not in app.admins:
buttons += [
[
InlineKeyboardButton(
text=locale("sub_block", "button", locale=configGet("locale")),
callback_data=f"sub_block_{msg.from_user.id}",
)
]
]
PosterUser(msg.from_user.id).limit()
if msg.from_user.id != app.owner:
await msg.reply_text(
locale("sub_sent", "message", locale=user_locale),
disable_notification=True,
quote=True,
)
await msg.copy(
app.owner, caption=caption, reply_markup=InlineKeyboardMarkup(buttons)
)
except AttributeError:
logWrite(f"from_user in function get_submission does not seem to contain id")

603
poster.py
View File

@@ -1,77 +1,31 @@
from os import sep, remove, getpid, listdir from datetime import datetime
from random import choice from os import getpid, path
from shutil import move from sys import exit
from sys import argv, exit
from threading import Thread
from time import time from time import time
from traceback import format_exc from traceback import format_exc
from pathlib import Path from modules.api_client import authorize
from modules.logging import logWrite
from modules.utils import configGet, jsonLoad, jsonSave, killProc, locale
# Args =====================================================================================================================================
if "--move-sent" in argv:
for entry in jsonLoad(configGet("index", "locations"))["sent"]:
try:
move(configGet("queue", "locations")+sep+entry, configGet("sent", "locations")+sep+entry)
except FileNotFoundError:
logWrite(locale("move_sent_doesnt_exist", "console", locale=configGet("locale_log")).format(entry))
except Exception as exp:
logWrite(locale("move_sent_doesnt_exception", "console", locale=configGet("locale_log")).format(entry, exp))
logWrite(locale("move_sent_completed", "console", locale=configGet("locale_log")))
if "--cleanup" in argv:
if "--confirm" in argv:
index = jsonLoad(configGet("index", "locations"))
for entry in index["sent"]:
try:
try:
remove(configGet("queue", "locations")+sep+entry)
except FileNotFoundError:
pass
try:
remove(configGet("sent", "locations")+sep+entry)
except FileNotFoundError:
pass
except Exception as exp:
logWrite(locale("cleanup_exception", "console", locale=configGet("locale_log")).format(entry, exp))
jsonSave(index, jsonLoad(configGet("index", "locations")))
logWrite(locale("cleanup_completed", "console", locale=configGet("locale_log")))
else:
logWrite(locale("cleanup_unathorized", "console", locale=configGet("locale_log")))
if "--cleanup-index" in argv:
if "--confirm" in argv:
index = jsonLoad(configGet("index", "locations"))
index["sent"] = []
jsonSave(index, jsonLoad(configGet("index", "locations")))
logWrite(locale("cleanup_index_completed", "console", locale=configGet("locale_log")))
else:
logWrite(locale("cleanup_index_unathorized", "console", locale=configGet("locale_log")))
if "--norun" in argv:
logWrite(locale("passed_norun", "console", locale=configGet("locale_log")))
exit()
#===========================================================================================================================================
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 =================================================================================================================================== # Import ===================================================================================================================================
try: try:
import schedule # type: ignore from dateutil.relativedelta import relativedelta
from pyrogram import Client, filters, idle # type: ignore from pyrogram.errors import bad_request_400
from pyrogram.types import ChatPermissions, ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton, BotCommand, BotCommandScopeChat # type: ignore from pyrogram.sync import idle
from pyrogram.raw.types import UpdateChannelMessageForwards, InputChannel, InputPeerChannel
from pyrogram.raw.functions.stats import GetMessagePublicForwards from modules.app import app
except ModuleNotFoundError: except ModuleNotFoundError:
print(locale("deps_missing", "console", locale=configGet("locale_log")), flush=True) print(locale("deps_missing", "console", locale=configGet("locale")), flush=True)
exit() exit()
#=========================================================================================================================================== # ===========================================================================================================================================
pid = getpid() pid = getpid()
app = Client("duptsiaposter", bot_token=configGet("bot_token", "bot"), api_id=configGet("api_id", "bot"), api_hash=configGet("api_hash", "bot")) version = 0.1
# Work in progress # Work in progress
# def check_forwards(app): # def check_forwards(app):
@@ -98,324 +52,34 @@ app = Client("duptsiaposter", bot_token=configGet("bot_token", "bot"), api_id=co
# logWrite("Could not get last posts forwards due to {0} with traceback {1}".format(str(exp), traceback.format_exc()), debug=True) # 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"): # if configGet("error", "reports"):
# app.send_message(configGet("admin"), traceback.format_exc()) # type: ignore # app.send_message(configGet("admin"), traceback.format_exc())
# pass # pass
def send_content():
# Send post to channel
try:
index = jsonLoad(configGet("index", "locations"))
list_queue = listdir(configGet("queue", "locations"))
for file in list_queue:
if not file in index["sent"]:
ext_match = False
for ext in configGet("photo", "posting", "extensions"):
if file.endswith(ext):
ext_match = True
ext_type = "photo"
break
for ext in configGet("video", "posting", "extensions"):
if file.endswith(ext):
ext_match = True
ext_type = "video"
break
if not ext_match:
list_queue.remove(file)
else:
list_queue.remove(file)
if len(list_queue) > 0:
candidate_file = choice(list_queue)
candidate = configGet("queue", "locations")+sep+candidate_file
else:
logWrite(locale("post_empty", "console", locale=configGet("locale_log")))
if configGet("error", "reports"):
app.send_message(configGet("admin"), locale("post_empty", "message", locale=configGet("locale"))) # type: ignore
return
if candidate_file in index["captions"]:
caption = index["captions"][candidate_file]
else:
caption = ""
if configGet("enabled", "caption"):
if configGet("link", "caption") != None:
caption = f"{caption}\n\n[{configGet('text', 'caption')}]({configGet('link', 'caption')})"
else:
caption = f"{caption}\n\n{configGet('text', 'caption')}"
else:
caption = caption
if ext_type == "photo": # type: ignore
if configGet("enabled", "caption"):
if configGet("link", "caption") != None:
sent = app.send_photo(configGet("channel", "posting"), candidate, caption=caption, disable_notification=configGet("silent", "posting")) # type: ignore
else:
sent = app.send_photo(configGet("channel", "posting"), candidate, caption=caption, disable_notification=configGet("silent", "posting")) # type: ignore
else:
sent = app.send_photo(configGet("channel", "posting"), candidate, caption=caption, disable_notification=configGet("silent", "posting")) # type: ignore
elif ext_type == "video": # type: ignore
if configGet("enabled", "caption"):
if configGet("link", "caption") != None:
sent = app.send_video(configGet("channel", "posting"), candidate, caption=caption, disable_notification=configGet("silent", "posting")) # type: ignore
else:
sent = app.send_video(configGet("channel", "posting"), candidate, caption=caption, disable_notification=configGet("silent", "posting")) # type: ignore
else:
sent = app.send_video(configGet("channel", "posting"), candidate, caption=caption, disable_notification=configGet("silent", "posting")) # type: ignore
else:
return
index["sent"].append(candidate_file)
index["last_id"] = sent.id
jsonSave(index, configGet("index", "locations"))
if configGet("move_sent", "posting"):
move(candidate, configGet("sent", "locations")+sep+candidate_file)
logWrite(locale("post_sent", "console", locale=configGet("locale_log")).format(candidate, ext_type, str(configGet("channel", "posting")), caption.replace("\n", "%n"), str(configGet("silent", "posting")))) # type: ignore
if configGet("sent", "reports"):
app.send_message(configGet("admin"), f"Posted `{candidate_file}`", disable_web_page_preview=True, reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton(locale("post_view", "button", locale=configGet("locale")), url=sent.link)] # type: ignore
])) # type: ignore
except Exception as exp:
logWrite(locale("post_exception", "console", locale=configGet("locale_log")).format(str(exp), format_exc()))
if configGet("error", "reports"):
app.send_message(configGet("admin"), locale("post_exception", "message", locale=configGet("locale")).format(exp, traceback.format_exc())) # type: ignore
pass
# Work in progress
# Check last posts forwards
# check_forwards(app)
if configGet("submit", "mode"):
@app.on_message(~ filters.scheduled & filters.command(["start"], prefixes="/"))
def cmd_start(app, msg):
if msg.from_user.id not in jsonLoad(configGet("blocked", "locations")):
msg.reply_text(locale("start", "message", locale=msg.from_user.language_code))
if configGet("submit", "mode"):
@app.on_message(~ filters.scheduled & filters.command(["rules", "help"], prefixes="/"))
def cmd_rules(app, msg):
if msg.from_user.id not in jsonLoad(configGet("blocked", "locations")):
msg.reply_text(locale("rules", "message", locale=msg.from_user.language_code))
# Work in progress # Work in progress
# @app.on_message(~ filters.scheduled & filters.command(["forwards"], prefixes="/")) # @app.on_message(~ filters.scheduled & filters.command(["forwards"], prefixes="/"))
# def cmd_forwards(app, msg): # def cmd_forwards(app, msg):
# check_forwards(app) # check_forwards(app)
@app.on_message(~ filters.scheduled & filters.command(["kill", "die", "reboot"], prefixes=["", "/"]))
def cmd_kill(app, msg):
if msg.from_user.id == configGet("admin"): from plugins.callbacks.shutdown import *
logWrite(locale("shutdown", "console", locale=configGet("locale_log")).format(str(pid)))
msg.reply_text(locale("shutdown", "message", locale=configGet("locale")).format(str(pid)))
killProc(pid)
# Imports ==================================================================================================================================
# Submission ===================================================================================================================================== from plugins.commands.general import *
def subLimit(user):
submit = jsonLoad(configGet("submit", "locations"))
submit[str(user.id)] = time()
jsonSave(submit, configGet("submit", "locations"))
def subLimited(user):
if user.id == configGet("admin"):
return False
else:
submit = jsonLoad(configGet("submit", "locations"))
if str(user.id) in submit:
if (time() - submit[str(user.id)]) < configGet("timeout", "submission"):
return True
else:
return False
else:
return False
def subBlock(user):
blocked = jsonLoad(configGet("blocked", "locations"))
if user not in blocked:
blocked.append(user)
jsonSave(blocked, configGet("blocked", "locations"))
def subUnblock(user):
blocked = jsonLoad(configGet("blocked", "locations"))
if user in blocked:
blocked.remove(user)
jsonSave(blocked, configGet("blocked", "locations"))
if configGet("submit", "mode"): if configGet("submit", "mode"):
@app.on_message(~ filters.scheduled & filters.photo | filters.video | filters.animation | filters.document) from plugins.callbacks.nothing import *
def get_submission(_, msg): from plugins.callbacks.submission import *
try: from plugins.commands.mode_submit import *
if msg.from_user.id not in jsonLoad(configGet("blocked", "locations")): from plugins.handlers.submission import *
user_locale = msg.from_user.language_code
if not subLimited(msg.from_user):
if msg.document != None: if configGet("post", "mode"):
if msg.document.mime_type not in configGet("mime_types", "submission"): from plugins.commands.photos import *
logWrite(locale("sub_mime_not_allowed", "console", locale=configGet("locale_log")).format(str(msg.from_user.id), msg.document.mime_type))
msg.reply_text(locale("mime_not_allowed", "message", locale=user_locale), quote=True)
return
if msg.document.file_size > configGet("file_size", "submission"):
logWrite(locale("sub_document_too_large", "console", locale=configGet("locale_log")).format(str(msg.from_user.id), str(msg.document.file_size), str(configGet("file_size", "submission"))))
msg.reply_text(locale("document_too_large", "message", locale=user_locale).format(str(configGet("file_size", "submission")/1024/1024)), quote=True)
return
if msg.video != None: # if configGet("api_based", "mode"):
if msg.video.file_size > configGet("file_size", "submission"): # from modules.api_client import authorize
logWrite(locale("sub_document_too_large", "console", locale=configGet("locale_log")).format(str(msg.from_user.id), str(msg.document.file_size), str(configGet("file_size", "submission")))) # ===========================================================================================================================================
msg.reply_text(locale("document_too_large", "message", locale=user_locale).format(str(configGet("file_size", "submission")/1024/1024)), quote=True)
return
buttons = [
[
InlineKeyboardButton(text=locale("sub_yes", "button", locale=configGet("locale")), callback_data=f"sub_yes_{msg.from_user.id}_{msg.id}")
]
]
if msg.caption != None:
caption = str(msg.caption)
buttons[0].append(
InlineKeyboardButton(text=locale("sub_yes_caption", "button", locale=configGet("locale")), callback_data=f"sub_yes_{msg.from_user.id}_{msg.id}_caption")
)
buttons[0].append(
InlineKeyboardButton(text=locale("sub_no", "button", locale=configGet("locale")), callback_data=f"sub_no_{msg.from_user.id}_{msg.id}")
)
else:
caption = ""
buttons[0].append(
InlineKeyboardButton(text=locale("sub_no", "button", locale=configGet("locale")), callback_data=f"sub_no_{msg.from_user.id}_{msg.id}")
)
caption += locale("sub_by", "message", locale=locale(configGet("locale")))
if msg.from_user.first_name != None:
caption += f" {msg.from_user.first_name}"
if msg.from_user.last_name != None:
caption += f" {msg.from_user.last_name}"
if msg.from_user.username != None:
caption += f" (@{msg.from_user.username})"
if msg.from_user.phone_number != None:
caption += f" ({msg.from_user.phone_number})"
if msg.from_user.id != configGet("admin"):
buttons += [
[
InlineKeyboardButton(text=locale("sub_block", "button", locale=configGet("locale")), callback_data=f"sub_block_{msg.from_user.id}")
]
# [
# InlineKeyboardButton(text=locale("sub_unblock", "button", locale=configGet("locale")), callback_data=f"sub_unblock_{msg.from_user.id}")
# ]
]
msg.reply_text(locale("sub_sent", "message", locale=user_locale), quote=True)
subLimit(msg.from_user)
logWrite(locale("sub_received", "console", locale=configGet("locale_log")).format(str(msg.from_user.id), str(msg.caption)))
msg.copy(configGet("admin"), caption=caption, reply_markup=InlineKeyboardMarkup(buttons))
else:
logWrite(locale("sub_cooldown", "console", locale=configGet("locale_log")).format(str(msg.from_user.id)))
msg.reply_text(locale("sub_cooldown", "message", locale=user_locale).format(str(configGet("timeout", "submission"))))
except AttributeError:
logWrite(locale("sub_received", "console", locale=configGet("locale_log")))
@app.on_callback_query(filters.regex("sub_yes_[\s\S]*_[\s\S]*")) # type: ignore
def callback_query_yes(app, clb): # type: ignore
fullclb = clb.data.split("_")
user_locale = clb.from_user.language_code
try:
submission = app.get_messages(int(fullclb[2]), int(fullclb[3]))
except:
logWrite(locale("sub_msg_unavail", "console", locale=configGet("locale_log")).format(fullclb[3], fullclb[2]))
clb.answer(text=locale("sub_msg_unavail", "message", locale=user_locale), show_alert=True)
return
try:
logWrite(locale("sub_media_downloading", "console", locale=configGet("locale_log")).format(fullclb[3], fullclb[2]))
media = app.download_media(submission, file_name=configGet("queue", "locations")+sep)
if clb.data.endswith("_caption"):
index = jsonLoad(configGet("index", "locations"))
index["captions"][Path(media).name] = submission.caption
jsonSave(index, configGet("index", "locations"))
logWrite(locale("sub_media_downloaded", "console", locale=configGet("locale_log")).format(fullclb[3], fullclb[2]))
except:
logWrite(locale("sub_media_unavail", "console", locale=configGet("locale_log")).format(fullclb[3], fullclb[2]))
clb.answer(text=locale("sub_media_unavail", "message", locale=user_locale), show_alert=True)
return
logWrite(locale("sub_accepted", "console", locale=configGet("locale_log")).format(fullclb[3], fullclb[2]))
submission.reply_text(locale("sub_yes", "message", locale=submission.from_user.language_code), quote=True)
edited_markup = clb.message.reply_markup.inline_keyboard
edited_markup[0] = [InlineKeyboardButton(text=locale("accepted", "button", locale=configGet("locale")), callback_data=f"sub_done")]
clb.message.edit(text=clb.message.caption, reply_markup=InlineKeyboardMarkup(edited_markup))
clb.answer(text=locale("sub_yes", "callback", locale=user_locale).format(fullclb[2]), show_alert=True)
@app.on_callback_query(filters.regex("sub_no_[\s\S]*_[\s\S]*")) # type: ignore
def callback_query_no(app, clb): # type: ignore
fullclb = clb.data.split("_")
user_locale = clb.from_user.language_code
try:
submission = app.get_messages(int(fullclb[2]), int(fullclb[3]))
except:
logWrite(locale("sub_msg_unavail", "console", locale=configGet("locale_log")).format(fullclb[3], fullclb[2]))
clb.answer(text=locale("sub_msg_unavail", "message", locale=user_locale), show_alert=True)
return
logWrite(locale("sub_declined", "console", locale=configGet("locale_log")).format(fullclb[3], fullclb[2]))
submission.reply_text(locale("sub_no", "message", locale=submission.from_user.language_code), quote=True)
edited_markup = clb.message.reply_markup.inline_keyboard
edited_markup[0] = [InlineKeyboardButton(text=locale("declined", "button", locale=configGet("locale")), callback_data=f"sub_done")]
clb.message.edit(text=clb.message.caption, reply_markup=InlineKeyboardMarkup(edited_markup))
clb.answer(text=locale("sub_no", "callback", locale=user_locale).format(fullclb[2]), show_alert=True)
@app.on_callback_query(filters.regex("sub_block_[\s\S]*")) # type: ignore
def callback_query_block(app, clb): # type: ignore
fullclb = clb.data.split("_")
user_locale = clb.from_user.language_code
app.send_message(int(fullclb[2]), locale("sub_blocked", "message", locale=configGet("locale")))
subBlock(int(fullclb[2]))
logWrite(locale("sub_blocked", "console", locale=configGet("locale_log")).format(fullclb[2]))
edited_markup = clb.message.reply_markup.inline_keyboard
edited_markup[1] = [InlineKeyboardButton(text=locale("sub_unblock", "button", locale=configGet("locale")), callback_data=f"sub_unblock_{fullclb[2]}")]
clb.message.edit(text=clb.message.caption, reply_markup=InlineKeyboardMarkup(edited_markup))
clb.answer(text=locale("sub_block", "callback", locale=user_locale).format(fullclb[2]), show_alert=True)
@app.on_callback_query(filters.regex("sub_unblock_[\s\S]*")) # type: ignore
def callback_query_unblock(app, clb): # type: ignore
fullclb = clb.data.split("_")
user_locale = clb.from_user.language_code
app.send_message(int(fullclb[2]), locale("sub_unblocked", "message", locale=configGet("locale")))
subUnblock(int(fullclb[2]))
logWrite(locale("sub_unblocked", "console", locale=configGet("locale_log")).format(fullclb[2]))
edited_markup = clb.message.reply_markup.inline_keyboard
edited_markup[1] = [InlineKeyboardButton(text=locale("sub_block", "button", locale=configGet("locale")), callback_data=f"sub_block_{fullclb[2]}")]
clb.message.edit(text=clb.message.caption, reply_markup=InlineKeyboardMarkup(edited_markup))
clb.answer(text=locale("sub_unblock", "callback", locale=user_locale).format(fullclb[2]), show_alert=True)
@app.on_callback_query(filters.regex("sub_done")) # type: ignore
def callback_query_block(app, clb): # type: ignore
user_locale = clb.from_user.language_code
clb.answer(text=locale("sub_done", "callback", locale=user_locale), show_alert=True)
#===========================================================================================================================================
# Work in progress # Work in progress
# Handle new forwards # Handle new forwards
@@ -428,66 +92,175 @@ def callback_query_block(app, clb): # type: ignore
# # else: # # else:
# # logWrite(f"Got raw update of type {type(update)} with contents {update}", debug=True) # # logWrite(f"Got raw update of type {type(update)} with contents {update}", debug=True)
if configGet("post", "mode"): # async def main():
for entry in configGet("time", "posting"): # await app.start()
schedule.every().day.at(entry).do(send_content)
def background_task(): # logWrite(locale("startup", "console", locale=configGet("locale")).format(str(pid)))
try:
while True:
try:
schedule.run_pending()
time.sleep(1)
except:
pass
except Exception as exp:
logWrite(locale("exception_occured", "console", locale=configGet("locale_log")).format(exp))
except KeyboardInterrupt:
logWrite(locale("keyboard_interrupt", "console", locale=configGet("locale_log")))
if configGet("shutdown", "reports"):
app.send_message(configGet("admin"), locale("shutdown", "message", locale=configGet("locale")).format(str(pid))) # type: ignore
killProc(pid)
if __name__ == "__main__": # if configGet("startup", "reports"):
# await app.send_message(configGet("admin"), locale("startup", "message", locale=configGet("locale")).format(str(pid)))
logWrite(locale("startup", "console", locale=configGet("locale_log")).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()
app.start() # type: ignore
if configGet("startup", "reports"): if configGet("startup", "reports"):
app.send_message(configGet("admin"), locale("startup", "message", locale=configGet("locale")).format(str(pid))) # type: ignore 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"): if configGet("post", "mode"):
t = Thread(target=background_task) scheduler.start()
t.start()
if configGet("submit", "mode"): try:
# Registering user commands token = await authorize()
for entry in listdir(configGet("locale", "locations")):
if entry.endswith(".json"):
commands_list = []
for command in configGet("commands"):
commands_list.append(BotCommand(command, locale(command, "commands", locale=entry.replace(".json", ""))))
app.set_bot_commands(commands_list, language_code=entry.replace(".json", "")) # type: ignore
# Registering user commands for fallback locale if (
commands_list = [] len(
for command in configGet("commands"): (
commands_list.append(BotCommand(command, locale(command, "commands", locale=configGet("locale_fallback")))) await (
app.set_bot_commands(commands_list) # type: ignore 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()}")
# Registering admin commands await idle()
commands_admin_list = []
if configGet("submit", "mode"):
for command in configGet("commands"):
commands_admin_list.append(BotCommand(command, locale(command, "commands", locale=configGet("locale"))))
for command in configGet("commands_admin"):
commands_admin_list.append(BotCommand(command, locale(command, "commands_admin", locale=configGet("locale"))))
app.set_bot_commands(commands_admin_list, scope=BotCommandScopeChat(chat_id=configGet("admin"))) # type: ignore
idle() 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."
)
app.send_message(configGet("admin"), locale("shutdown", "message", locale=configGet("locale")).format(str(pid))) # type: ignore makedirs(configGet("cache", "locations"), exist_ok=True)
logWrite(locale("shutdown", "console", locale=configGet("locale_log")).format(str(pid))) jsonSave(
{"timestamp": time()},
path.join(configGet("cache", "locations"), "shutdown_time"),
)
killProc(pid) 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 +0,0 @@
ujson

View File

@@ -1,3 +1,12 @@
schedule python_dateutil==2.8.2
pyrogram>=2.0.0 apscheduler==3.10.1
psutil 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