├── .env ├── .env.example ├── .github ├── codeql │ └── codeql-config.yml ├── dependabot.yml ├── labeler.yml ├── stale.yml └── workflows │ ├── codeql-analysis.yml │ ├── greetings.yml │ ├── heroku-deploy.yml │ ├── label.yml │ ├── metrics.yml │ ├── pylint.yml │ ├── python-app.yml │ ├── python.yml │ ├── stale.yml │ ├── update-l10n-sources.yml │ ├── update-l10n-translations.yml │ └── update-l10n.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── _config.yml ├── app.json ├── bot.py ├── crowdin.yml ├── deploy.sh ├── heroku.yml ├── images ├── icon.png ├── icon.svg ├── icon_beta.png └── icon_dev.png ├── input.txt ├── locale ├── af_ZA │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── am_ET │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── ar_SA │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── ca_ES │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── cs_CZ │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── da_DK │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── de_DE │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── el_GR │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── en_GB │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── en_US │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── es_ES │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── fi_FI │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── fr_FR │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── he_IL │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── hu_HU │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── it_IT │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── ja_JP │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── ko_KR │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── ky_KG │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── nl_NL │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── no_NO │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── pl_PL │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── pt_BR │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── pt_PT │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── ro_RO │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── ru_RU │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── si_LK │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── sr_SP │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── sv_SE │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── tr_TR │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── uk_UA │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── vi_VN │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── zh_CN │ └── LC_MESSAGES │ │ └── pdf_bot.po ├── zh_HK │ └── LC_MESSAGES │ │ └── pdf_bot.po └── zh_TW │ └── LC_MESSAGES │ └── pdf_bot.po ├── nltk.txt ├── pdf-bot.metrics.svg ├── pdf_bot ├── __init__.py ├── commands │ ├── __init__.py │ ├── compare.py │ ├── merge.py │ ├── photo.py │ ├── text.py │ └── watermark.py ├── constants.py ├── feedback.py ├── files │ ├── __init__.py │ ├── compress.py │ ├── crop.py │ ├── crypto.py │ ├── document.py │ ├── file.py │ ├── ocr.py │ ├── photo.py │ ├── rename.py │ ├── rotate.py │ ├── scale.py │ ├── split.py │ ├── text.py │ └── utils.py ├── language.py ├── mq_bot.py ├── payment.py ├── stats.py ├── store.py ├── url.py └── utils.py ├── pyproject.toml ├── renovate.json ├── requirements.txt ├── runtime.txt └── start.sh /.env: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | TELE_TOKEN= os.environ.get("YOUR_TELEGRAM_BOT_TOKEN","") 4 | DEV_TELE_ID= int(os.environ.get("YOUR_TELEGRAM_USER_ID","")) 5 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | TELE_TOKEN= 2 | DEV_TELE_ID= 3 | 4 | -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yml: -------------------------------------------------------------------------------- 1 | paths: 2 | - pdf_bot/ 3 | 4 | paths-ignore: 5 | - "**/site-packages/" 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | repo: 2 | - '*' 3 | 4 | test: 5 | - pdf_bot/**/*.spec.js 6 | 7 | source: 8 | - any: ['pdf_bot/**/*', '!pdf_bot/files/*'] 9 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 3 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 2 5 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 6 | onlyLabels: question 7 | # Label to use when marking an issue as stale 8 | staleLabel: stale 9 | # Comment to post when marking an issue as stale. Set to `false` to disable 10 | markComment: false 11 | # Comment to post when closing a stale issue. Set to `false` to disable 12 | closeComment: > 13 | This issue has been automatically closed due to inactivity. Feel free to comment in order to reopen. 14 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [master] 17 | pull_request: 18 | branches: [master] 19 | paths-ignore: 20 | - locale/ 21 | - "**/*.md" 22 | - "**/*.txt" 23 | - "**/*.yml" 24 | schedule: 25 | - cron: "16 2 * * 3" 26 | 27 | jobs: 28 | analyze: 29 | name: Analyze 30 | runs-on: ubuntu-latest 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["python"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v2 47 | with: 48 | languages: ${{ matrix.language }} 49 | config-file: ./.github/codeql/codeql-config.yml 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@v2 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 https://git.io/JvXDl 62 | 63 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 64 | # and modify them (or add more) to build your code if your project 65 | # uses a compiled language 66 | 67 | #- run: | 68 | # make bootstrap 69 | # make release 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | pull-requests: write 10 | steps: 11 | - uses: actions/first-interaction@v1 12 | with: 13 | repo-token: ${{ secrets.GITHUB_TOKEN }} 14 | pr-message: "Thanks a lot Sir/Ma'am for contributing on the repo ❤️" 15 | -------------------------------------------------------------------------------- /.github/workflows/heroku-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Manually Deploy to heroku 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | deploy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: akhileshns/heroku-deploy@v3.14.15 11 | with: 12 | heroku_api_key: ${{secrets.HEROKU_API_KEY}} 13 | heroku_app_name: ${{secrets.HEROKU_APP_NAME}} 14 | heroku_email: ${{secrets.HEROKU_EMAIL}} 15 | region: "eu" 16 | usedocker: true 17 | docker_heroku_process_type: worker 18 | env: 19 | HD_TELE_TOKEN: ${{secrets.TELE_TOKEN}} 20 | HD_DEV_TELE_ID: ${{secrets.DEV_TELE_ID}} 21 | HD_GOOGLE_APPLICATION_CREDENTIALS: ${{secrets.GOOGLE_APPLICATION_CREDENTIALS}} 22 | -------------------------------------------------------------------------------- /.github/workflows/label.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | - pull_request_target 4 | 5 | jobs: 6 | triage: 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/labeler@v5 13 | with: 14 | repo-token: "${{ secrets.GITHUB_TOKEN }}" -------------------------------------------------------------------------------- /.github/workflows/metrics.yml: -------------------------------------------------------------------------------- 1 | name: Metrics 2 | on: 3 | # Schedule updates (At 00:00 on Sunday) 4 | schedule: [{cron: "0 0 * * 0"}] 5 | workflow_dispatch: 6 | push: {branches: ["master", "main"]} 7 | jobs: 8 | github-metrics: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: lowlighter/metrics@latest 12 | with: 13 | token: ${{ secrets.GIT_HUB_TOKEN }} 14 | filename: pdf-bot.metrics.svg 15 | template: repository 16 | user: MrBotDeveloper 17 | repo: PDF-Bot 18 | plugin_lines: yes 19 | plugin_followup: yes 20 | -------------------------------------------------------------------------------- /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: PyLint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | PEP8: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | 11 | - name: Setup Python 12 | uses: actions/setup-python@v5 13 | with: 14 | python-version: 3.8 15 | 16 | - name: Install Python lint libraries 17 | run: | 18 | pip install autopep8 autoflake isort black 19 | - name: Check for showstoppers 20 | run: | 21 | autopep8 --verbose --in-place --recursive --aggressive --aggressive --ignore=W605. *.py 22 | - name: Remove unused imports and variables 23 | run: | 24 | autoflake --in-place --recursive --remove-all-unused-imports --remove-unused-variables --ignore-init-module-imports . 25 | - name: lint with isort and black 26 | run: | 27 | isort . 28 | black . 29 | - uses: stefanzweifel/git-auto-commit-action@v5 30 | with: 31 | commit_message: 'Auto Fixes: Code Formatting' 32 | commit_options: '--no-verify' 33 | repository: . 34 | commit_user_name: MrBotDeveloper 35 | commit_user_email: botmakerdeveloper@gmail.com 36 | commit_author: Mr. Developer 37 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | name: PDF-Bot2 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python 3.9 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: 3.9 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install flake8 pytest 24 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 25 | - name: Lint with flake8 26 | run: | 27 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 28 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 29 | - name: Setup 30 | run: | 31 | pybabel compile -D pdf_bot -d locale 32 | -------------------------------------------------------------------------------- /.github/workflows/python.yml: -------------------------------------------------------------------------------- 1 | name: PDF-Bot 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ ubuntu-latest ] 17 | python-version: [ 3.7, 3.8, 3.9 ] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | python -m pip install flake8 pytest 29 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 30 | - name: Lint with flake8 31 | run: | 32 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 33 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 34 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Check Stale Pulls and Issues 2 | 3 | on: 4 | schedule: 5 | - cron: '29 12 * * *' 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | pull-requests: write 14 | 15 | steps: 16 | - uses: actions/stale@v9 17 | with: 18 | repo-token: ${{ secrets.GIT_HUB_TOKEN }} 19 | stale-issue-message: 'This issue has been automatically closed due to inactivity. Feel free to comment in order to reopen.' 20 | stale-pr-message: 'This Pull Request has been automatically closed due to inactivity. Feel free to reopen.' 21 | stale-issue-label: 'Auto-Closed' 22 | stale-pr-label: 'Auto-Closed' 23 | -------------------------------------------------------------------------------- /.github/workflows/update-l10n-sources.yml: -------------------------------------------------------------------------------- 1 | name: update-l10n-sources 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | update-l10n-sources: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🛎️ 12 | uses: actions/checkout@v4 13 | with: 14 | token: ${{ secrets.PAT }} 15 | 16 | - name: Setup Python 🐍 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: "3.13" 20 | 21 | - name: Restore pip cache 💾 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/.cache/pip 25 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 26 | restore-keys: | 27 | ${{ runner.os }}-pip- 28 | - name: Install Babel ⚙️ 29 | run: | 30 | pip3 install -U pip 31 | pip3 install Babel==2.9.0 32 | - name: Update localization source file 📄 33 | run: | 34 | pybabel extract bot.py pdf_bot/ -o locale/pdf_bot.pot 35 | pybabel update -l locale -i locale/pdf_bot.pot -o locale/en_GB/LC_MESSAGES/pdf_bot.po 36 | echo NUM_DIFFS=$(git diff --shortstat | egrep -o '[0-9]+ i' | egrep -o '[0-9]+') >> $GITHUB_ENV 37 | - name: Commit changes 🆕 38 | if: env.NUM_DIFFS > 1 39 | uses: stefanzweifel/git-auto-commit-action@v5 40 | with: 41 | commit_message: Update localization source file 42 | file_pattern: locale/en_GB/LC_MESSAGES/pdf_bot.po 43 | push_options: --force 44 | 45 | - name: Upload sources and download translations 🌐 46 | if: env.NUM_DIFFS > 1 47 | uses: crowdin/github-action@v2.7.0 48 | with: 49 | upload_sources: true 50 | upload_translations: false 51 | download_translations: true 52 | create_pull_request: true 53 | pull_request_labels: "automerge, l10n" 54 | config: ./crowdin.yml 55 | source: locale/en_GB/LC_MESSAGES/pdf_bot.po 56 | translation: /locale/%locale_with_underscore%/LC_MESSAGES/pdf_bot.po 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GIT_HUB_TOKEN }} 59 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 60 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 61 | -------------------------------------------------------------------------------- /.github/workflows/update-l10n-translations.yml: -------------------------------------------------------------------------------- 1 | name: update-l10n-translations 2 | 3 | on: 4 | schedule: 5 | - cron: "0 8 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | update-l10n-translations: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 🛎️ 13 | uses: actions/checkout@v4 14 | with: 15 | token: ${{ secrets.PAT }} 16 | 17 | - name: Download translations 🌐 18 | uses: crowdin/github-action@v2.7.0 19 | with: 20 | upload_sources: false 21 | upload_translations: false 22 | download_translations: true 23 | create_pull_request: true 24 | pull_request_labels: "automerge, l10n" 25 | config: ./crowdin.yml 26 | source: locale/en_GB/LC_MESSAGES/pdf_bot.po 27 | translation: /locale/%locale_with_underscore%/LC_MESSAGES/pdf_bot.po 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GIT_HUB_TOKEN }} 30 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 31 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/update-l10n.yml: -------------------------------------------------------------------------------- 1 | name: Update-l10n 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | update-l10n: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🛎️ 12 | uses: actions/checkout@v4 13 | with: 14 | persist-credentials: false 15 | 16 | - name: Setup Python 🐍 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: "3.13" 20 | 21 | - name: Restore pip cache 💾 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/.cache/pip 25 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 26 | restore-keys: | 27 | ${{ runner.os }}-pip- 28 | 29 | - name: Install Babel 🌐 30 | run: | 31 | pip3 install -U pip 32 | pip3 install Babel==2.9.0 33 | 34 | - name: Update localization source file 📄 35 | run: | 36 | pybabel extract bot.py pdf_bot/ -o locale/pdf_bot.pot 37 | pybabel update -l locale -i locale/pdf_bot.pot -o locale/en_GB/LC_MESSAGES/pdf_bot.po 38 | echo NUM_DIFFS=$(git diff --shortstat | egrep -o '[0-9]+ i' | egrep -o '[0-9]+') >> $GITHUB_ENV 39 | 40 | - name: Create Pull Request 🆕 41 | if: env.NUM_DIFFS > 1 42 | uses: peter-evans/create-pull-request@v7 43 | with: 44 | commit-message: Update localization source file 45 | branch: update/l10n-source-file 46 | title: Update localization source file 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/macos,visualstudiocode,python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,visualstudiocode,python 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### Python ### 35 | # Byte-compiled / optimized / DLL files 36 | __pycache__/ 37 | *.py[cod] 38 | *$py.class 39 | 40 | # C extensions 41 | *.so 42 | 43 | # Distribution / packaging 44 | .Python 45 | build/ 46 | develop-eggs/ 47 | dist/ 48 | downloads/ 49 | eggs/ 50 | .eggs/ 51 | lib/ 52 | lib64/ 53 | parts/ 54 | sdist/ 55 | var/ 56 | wheels/ 57 | pip-wheel-metadata/ 58 | share/python-wheels/ 59 | *.egg-info/ 60 | .installed.cfg 61 | *.egg 62 | MANIFEST 63 | 64 | # PyInstaller 65 | # Usually these files are written by a python script from a template 66 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 67 | *.manifest 68 | *.spec 69 | 70 | # Installer logs 71 | pip-log.txt 72 | pip-delete-this-directory.txt 73 | 74 | # Unit test / coverage reports 75 | htmlcov/ 76 | .tox/ 77 | .nox/ 78 | .coverage 79 | .coverage.* 80 | .cache 81 | nosetests.xml 82 | coverage.xml 83 | *.cover 84 | *.py,cover 85 | .hypothesis/ 86 | .pytest_cache/ 87 | pytestdebug.log 88 | 89 | # Translations 90 | 91 | *.pot 92 | *.json 93 | 94 | # Django stuff: 95 | *.log 96 | local_settings.py 97 | db.sqlite3 98 | db.sqlite3-journal 99 | 100 | # Flask stuff: 101 | instance/ 102 | .webassets-cache 103 | 104 | # Scrapy stuff: 105 | .scrapy 106 | 107 | # Sphinx documentation 108 | docs/_build/ 109 | doc/_build/ 110 | 111 | # PyBuilder 112 | target/ 113 | 114 | # Jupyter Notebook 115 | .ipynb_checkpoints 116 | 117 | # IPython 118 | profile_default/ 119 | ipython_config.py 120 | 121 | # pyenv 122 | .python-version 123 | 124 | # pipenv 125 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 126 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 127 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 128 | # install all needed dependencies. 129 | #Pipfile.lock 130 | 131 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 132 | __pypackages__/ 133 | 134 | # Celery stuff 135 | celerybeat-schedule 136 | celerybeat.pid 137 | 138 | # SageMath parsed files 139 | *.sage.py 140 | 141 | # Environments 142 | .env 143 | .venv 144 | env/ 145 | venv/ 146 | ENV/ 147 | env.bak/ 148 | venv.bak/ 149 | pythonenv* 150 | 151 | # Spyder project settings 152 | .spyderproject 153 | .spyproject 154 | 155 | # Rope project settings 156 | .ropeproject 157 | 158 | # mkdocs documentation 159 | /site 160 | 161 | # mypy 162 | .mypy_cache/ 163 | .dmypy.json 164 | dmypy.json 165 | 166 | # Pyre type checker 167 | .pyre/ 168 | 169 | # pytype static type analyzer 170 | .pytype/ 171 | 172 | # profiling data 173 | .prof 174 | 175 | ### VisualStudioCode ### 176 | .vscode/* 177 | !.vscode/tasks.json 178 | !.vscode/launch.json 179 | *.code-workspace 180 | 181 | ### VisualStudioCode Patch ### 182 | # Ignore all local history of files 183 | .history 184 | .ionide 185 | 186 | # End of https://www.toptal.com/developers/gitignore/api/macos,visualstudiocode,python 187 | 188 | demo_files/ 189 | keyfile.json -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pycqa/isort 3 | rev: 5.9.3 4 | hooks: 5 | - id: isort 6 | name: isort 7 | args: 8 | - --diff 9 | - --check-only 10 | - repo: https://github.com/psf/black 11 | rev: 21.5b1 12 | hooks: 13 | - id: black 14 | name: black 15 | args: 16 | - --diff 17 | - --check 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | 3 | branches: 4 | only: 5 | - master 6 | 7 | deploy: 8 | provider: heroku 9 | api_key: 10 | secure: WpJ3DzXgpFQ6bSwkS4QrrUlDk9JVoYgEUgYvOfN6y6aUOoYlJuErOU+6MRyLPQRkKquNGUxpVOKWU06OOmLpstjTfi6BTEH9da3IFuke8Sd3+HELYJF0A3Ae4amtMK9Ak7bsY8G75Zy9zE0RcLrAknRAu4aGi7X5F63mEyrZ0XH/lrESpD3e8RdCUR3F88jVtKuwYY7yG0K/qknJYHEpxhjqaDi9pLzXZsjddzFDM1wTFTbAXe7DIaJ6KtrCk+8UxKNHucrRakbXoE9CNFoRVJT5B8cWC+DdFw1bikKbE6IEA7R8cwZDE0Uo+gcl3I4c1c23247IeW0QMk3vrxAb0F8egCqGSK+QlSfi1pWDiRb9inVgU4LVA3i2GVHdrLsLh8p3Q0pjklJjOtfI0vyweiJJrZO0aL9IpwTUYx7hiDg9eiGAxIXV9XiBectfb0+6Kl9xxRVmh8QuX5hECC7o+YPXzOHZnPexb/XRc3O+yZX+XDGyzUC01eR8gOK/LRkT2ryKeXTxyrMMWLd3Wh1U+NF5Z532K9ZJQrldhty4/ErDRBaR5WEeQYfkN2lf9sKivulX1Xej2k0WgQA7SD3IxT0KL6Jk1aIjOEmNhqQIYgWNgq9pyrZ7/eCe/8sli1Yv24NCMDUi3oY93rUC9wO0hPjy9pIVJMSnCueRaB19Bns= 11 | on: master 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black" 3 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:latest 2 | 3 | ENV VIRTUAL_ENV "/venv" 4 | RUN python -m venv $VIRTUAL_ENV 5 | ENV PATH "$VIRTUAL_ENV/bin:$PATH" 6 | RUN . venv/bin/activate 7 | 8 | RUN python -m pip install --upgrade pip 9 | 10 | RUN apt-get update && apt-get install -y --no-install-recommends \ 11 | poppler-utils libcairo2 libpango-1.0-0 \ 12 | libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev shared-mime-info ocrmypdf \ 13 | && apt-get clean \ 14 | && rm -rf /var/lib/apt/lists/* 15 | 16 | WORKDIR /bot 17 | COPY . /bot 18 | RUN pip install -r requirements.txt 19 | 20 | RUN pybabel compile -D pdf_bot -d locale 21 | 22 | EXPOSE ${PORT} 23 | 24 | ENV APP_URL ${APP_URL} 25 | ENV TELE_TOKEN ${TELE_TOKEN} 26 | ENV DEV_TELE_ID ${DEV_TELE_ID} 27 | ENV GCP_CRED ${GCP_CRED} 28 | ENV GCP_KEY ${GCP_KEY} 29 | ENV SLACK_TOKEN ${SLACK_TOKEN} 30 | ENV STRIPE_TOKEN ${STRIPE_TOKEN} 31 | 32 | CMD ["python", "bot.py"] 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 zeshuaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python bot.py 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram PDF Bot 2 | 3 | [![Typing SVG](https://mdtype.herokuapp.com?font=Righteous&color=253AF7&size=31¢er=true&vCenter=true&width=500&height=38&lines=A+Powerful+Telegram+PDF+Bot.....+;Deployable+On+Heroku+%F0%9F%9A%80+....+;Repo+Modified+%E2%9C%85+By;%40MrBotDeveloper;Show+Your+%E2%9D%A4%EF%B8%8F;%E2%AD%90+the+repo;Follow+%40MrBotDeveloper+Now...+;For+More+%F0%9F%A5%B0)](https://github.com/MrBotDeveloper) 4 | 5 | [![Telegram Bot](https://img.shields.io/badge/Telegram-Bot-blue.svg)](https://github.com/MrBotDeveloper/PDF-Bot) 6 | [![MIT License](https://img.shields.io/github/license/MrBotDeveloper/telegram-pdf-bot.svg)](https://github.com/MrBotDeveloper/PDF-Bot/blob/master/LICENSE) 7 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 8 | [![Crowdin](https://badges.crowdin.net/telegram-pdf-bot/localized.svg)](https://crowdin.com/project/telegram-pdf-bot) 9 | [![Telegram Channel](https://img.shields.io/badge/Telegram-Channel-blue.svg)](https://t.me/NACBots) 10 | 11 | [![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=MrBotDeveloper&repo=PDF-Bot&theme=flag-india)](https://github.com/MrBotDeveloper/PDF-Bot) 12 | 13 | 14 | A Telegram bot that can: 15 | 16 | - Compress, crop, decrypt, encrypt, merge, preview, rename, rotate, scale and split PDF files 17 | - Compare text differences between two PDF files 18 | - Create PDF files from text messages 19 | - Add watermark to PDF files 20 | - Multiple languages support 21 | - Add text layers to PDF files to make them searchable with text 22 | - Extract images and text from PDF files 23 | - Convert PDF files into images 24 | - Beautify handwritten notes images into PDF files 25 | - Convert webpages and images into PDF files 26 | 27 | # Repo Special 😅 28 | ## What's Special in this repo & To-Do's ??? 29 | 30 | - [x] ~~Make It Heroku Deployable~~ 31 | - [x] ~~Add Detailed Guide to get GCP Credentials~~ 32 | - [ ] Add Private Use Feature 33 | - [ ] Add Password feature 🔑 for private use 34 | - [ ] Remove GCP and use another free Cloud Storage. 35 | - [ ] Add Broadcasting Feature 36 | - [ ] Make it more stable and fast 37 | 38 | ## Mandatory Vars.... 39 | 40 | ```vars.html 41 | DEV_TELE_ID - Your Telegram ID. 42 | TELE_TOKEN - Telegram Bot Token get from @BotFather 43 | GOOGLE_APPLICATION_CREDENTIALS - Your GCP Credentials get from Google Cloud 44 | ``` 45 | 46 | ## Where To Get The Mandatory Vars.. 47 | 48 | ```DEV_TELE_ID``` - Get it from [Thunder ⚡ Bot](https://t.me/Thunder_GMBot) by sending ```/id``` 49 | 50 | ```TELE_TOKEN``` - Get it from [@BotFather](https://t.me/BotFather) 51 | 52 | ```GOOGLE_APPLICATION_CREDENTIALS``` - Get it from [Google Cloud ☁️](https://console.cloud.google.com/freetrial) 53 | 54 | ## Optional Vars.... 55 | 56 | ```vars.txt 57 | STRIPE_TOKEN - Stripe.com token for receiving Donations. 58 | SLACK_TOKEN - slack.com api token to recieve Feedbacks on Slack.com if not entered you will recieve in your Telegram 59 | ``` 60 | 61 | ## Where To Get The Optional Vars... 62 | 63 | ```STRIPE_TOKEN``` - Get it from [stripe.com](https://stripe.com) 64 | 65 | ```SLACK_TOKEN``` - Get it from [slack.com](https://api.slack.com/tokens) 66 | 67 | ## Installation [ ⚠️ Click On Any Topic To Get it's Detailed Information ⚠️] 68 | 69 |
70 | Getting GCP Ceredinials ⚠️ Important ⚠️ 71 | 72 | ## Getting Started 73 | 74 | These instructions will get you a copy of the project up. 75 | 76 | ### Setup Database 77 | 78 | The bot uses [Datastore](https://cloud.google.com/datastore) on Google Cloud Platform (GCP). 79 | 80 | **Sir/Ma'am, Kindly 🤗 follow the below steps to create a valid GCP Credentials File :-** 81 | 82 | 1. Firstly Go to https://console.cloud.google.com/project 83 | 2. Create a Project. 84 | 3. Open http://console.developers.google.com/project/_/apiui/credential And Click on Create Ceredinials then Click on Service Account. 85 | 4. Enter all the required values. [At the Service Accounts, enter a Service account name and click Create. For Service account permissions, select Project, Owner.] 86 | 5. Select a service account. Click the 3 skewer bar and select Create Key. Select JSON, click Create. 87 | 6. Click Create. The credential file will be downloaded to your local computer or Any Device your are Using. 88 | 7. Upload the Project Credential file to the bots private repo. 89 | 8. Then Open https://console.cloud.google.com/datastore/setup . 90 | 9. And enable the FireStore Database. 91 | 10. Now open https://console.cloud.google.com/iam-admin/iam . 92 | 11. And set the service account's role to owner. **Note: If you can't see your service account in the list click on Add and add your service account with Owner as Role.** 93 | 12. Now Deploy your bot and set ```GOOGLE_APPLICATION_CREDENTIALS``` var with value as the File Name of the Ceredinials Json you uploaded in the repo in Step 7. 94 | 13. Congratulations 🎉 your bot has been Successfully Started 😊 So enjoy 🤗. 95 | 96 | ```alert.txt 97 | ⚠️ I will Recommend you to Use Only Google Chrome for generating Ceredinials Json as some browsers will not start the Download of the Json file in the Step 6 ⚠️ 98 | ``` 99 | 100 | **Don't Forget to Star 🌟 Repo if ❤️ The Repo and Follow [Me](https://github.com/MrBotDeveloper) to show your ❤️.** 101 | 102 |
103 |
104 | The Easy Way (Heroku) 105 | 106 | ## Follow these steps for a successful deployment..... 107 | - Star ⭐ the repo 😅 and import it as Private. 108 | - Upload your GCP Credentials in the root directory with name `GCP_FILE.json` 109 | - Click on the Below Deploy Button ✅ 110 | 111 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/MrBotDeveloper/PDF-Bot/tree/master) 112 | 113 | - Enter the Ceredinials and Click in deploy. 114 | - After Deployment Completed Click on Manage App & Turn on the Dynos.... 115 | - Booyah!! Your PDF Bot is successfully started. 116 | - Enjoy the Bot 🥳. 117 | ## Deploy using GitHub Workflows..... 118 | - Star ⭐ the repo 😅 and import it as Private. 119 | - Upload your GCP Credentials in the root directory with name `GCP_FILE.json` 120 | - Go to Project->Settings->Secrets and Click *New repository secret* and Add All the following Vars as the Repository Secrets. 121 | 122 | ```HEROKU_API_KEY```: Your Heroku Account API 123 | 124 | ```HEROKU_APP_NAME``` : Heroku App Name 125 | 126 | ```HEROKU_EMAIL``` : Your Heroku Email 📨 Id 127 | 128 | ```DEV_TELE_ID``` : Your Telegram ID. 129 | 130 | ```TELE_TOKEN``` : Telegram Bot Token get from @BotFather 131 | 132 | ```GOOGLE_APPLICATION_CREDENTIALS``` : Your GCP Credentials get from Google Cloud 133 | 134 | - Go To The Actions Tab and Choose ```Manually Deploy To Heroku``` and click on run workflow. 135 | 136 | ### Follow [me](https://github.com/MrBotDeveloper) if Love ❣️ the repo. 137 | 138 | 139 |
140 | 141 |
142 | Local Host 143 | 144 | ### OS Requirements 145 | 146 | Ubuntu 147 | 148 | ```sh 149 | apt-get install poppler-utils libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev shared-mime-info 150 | ``` 151 | 152 | macOS 153 | ```sh 154 | brew install libxml2 libxslt poppler cairo pango gdk-pixbuf libffi 155 | ``` 156 | 157 | ### Setup Virtual Environment 158 | 159 | Create a virtual environment with the following command: 160 | 161 | ```sh 162 | virtualenv venv 163 | source venv/bin/activate 164 | ``` 165 | 166 | ### Bot Requirements 167 | 168 | Run the following command to install the required packages: 169 | 170 | ```sh 171 | pip install -r requirements.txt 172 | ``` 173 | 174 | ### Compile the translation files 175 | 176 | Run the following command to compile all the translation files: 177 | 178 | ```sh 179 | pybabel compile -D pdf_bot -d locale/ 180 | ``` 181 | 182 | ### Setup Your Environment Variables 183 | 184 | Copy the `.env` example file and edit the variables within the file: 185 | 186 | ```sh 187 | cp .env.example .env 188 | ``` 189 | 190 | ### Running The Bot 191 | 192 | You can then start the bot with the following command: 193 | 194 | ```bash 195 | python bot.py 196 | ``` 197 | 198 | ### Follow [me](https://github.com/MrBotDeveloper) if Love ❣️ the repo. 199 | 200 |
201 | 202 | 203 | ## Follow [me](https://github.com/MrBotDeveloper) if Love ❣️ the repo. 204 | 205 | ## Found a Bug 🐛 206 | 207 | ```Feel free to create a pull or create a issue now and describe your issue freely.``` 208 | 209 |

210 | 211 | 212 | 213 |

214 | 215 | ## Credits 216 | 217 | - [Me 🥰](https://github.com/MrBotDeveloper) For making Deployable To Heroku 218 | - [@zeshuaro](https://github.com/zeshuaro) for his amazing Creation [Telegram PDF Bot](https://github.com/zeshuaro/telegram-pdf-bot) 219 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PDF Bot", 3 | "description": "Telegram's best Open Source ALL-In-One Multi Purpose RoBot for PDF editing 🤖.", 4 | "logo": "https://telegra.ph/file/7f5b5be68dd506edc4d5d.jpg", 5 | "keywords": ["telegram","best","heroku","PDF","bot"], 6 | "success_url": "https://github.com/MrBotDeveloper", 7 | "website": "https://mrbotdeveloper.github.io/PDF-Bot/", 8 | "repository": "https://github.com/MrBotDeveloper/PDF-Bot", 9 | "env": { 10 | "DEV_TELE_ID": { 11 | "description": "YOUR_TELEGRAM_USER_ID", 12 | "value": "", 13 | "required": true 14 | }, 15 | "TELE_TOKEN": { 16 | "description": "YOUR_TELEGRAM_BOT_TOKEN", 17 | "value": "", 18 | "required": true 19 | }, 20 | "GOOGLE_APPLICATION_CREDENTIALS": { 21 | "description": "Your GCP Credentials file path if in root directory just put file Name.", 22 | "value": "GCP_FILE.json", 23 | "required": true 24 | }, 25 | "STRIPE_TOKEN": { 26 | "description": "Get an token from stripe.com for receiving Donations 💰", 27 | "value": "" 28 | }, 29 | "SLACK_TOKEN": { 30 | "description": "To get all Feedbacks in slack.com not necessary if not entered you will receive feedbacks in telegram.", 31 | "value": "" 32 | } 33 | }, 34 | "stack": "container" 35 | } 36 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import logging 3 | import os 4 | import sys 5 | from threading import Thread 6 | 7 | from dotenv import load_dotenv 8 | from logbook import Logger, StreamHandler 9 | from logbook.compat import redirect_logging 10 | from telegram import ( 11 | InlineKeyboardButton, 12 | InlineKeyboardMarkup, 13 | MessageEntity, 14 | ParseMode, 15 | Update, 16 | ) 17 | from telegram.chataction import ChatAction 18 | from telegram.error import Unauthorized 19 | from telegram.ext import ( 20 | CallbackContext, 21 | CallbackQueryHandler, 22 | CommandHandler, 23 | Filters, 24 | MessageHandler, 25 | PreCheckoutQueryHandler, 26 | Updater, 27 | ) 28 | from telegram.ext import messagequeue as mq 29 | from telegram.utils.request import Request 30 | 31 | from pdf_bot import * 32 | 33 | load_dotenv() 34 | APP_URL = os.environ.get("APP_URL") 35 | PORT = int(os.environ.get("PORT", "8443")) 36 | TELE_TOKEN = os.environ.get("TELE_TOKEN_BETA", os.environ.get("TELE_TOKEN")) 37 | DEV_TELE_ID = int(os.environ.get("DEV_TELE_ID")) 38 | 39 | TIMEOUT = 20 40 | CALLBACK_DATA = "callback_data" 41 | 42 | 43 | def main(): 44 | # Setup logging 45 | logging.getLogger("pdfminer").setLevel(logging.WARNING) 46 | logging.getLogger("ocrmypdf").setLevel(logging.WARNING) 47 | redirect_logging() 48 | format_string = "{record.level_name}: {record.message}" 49 | StreamHandler( 50 | sys.stdout, format_string=format_string, level="INFO" 51 | ).push_application() 52 | log = Logger() 53 | 54 | q = mq.MessageQueue(all_burst_limit=3, all_time_limit_ms=3000) 55 | request = Request(con_pool_size=8) 56 | pdf_bot = MQBot(TELE_TOKEN, request=request, mqueue=q) 57 | 58 | # Create the EventHandler and pass it your bot's token. 59 | updater = Updater( 60 | bot=pdf_bot, 61 | use_context=True, 62 | request_kwargs={"connect_timeout": TIMEOUT, "read_timeout": TIMEOUT}, 63 | ) 64 | 65 | def stop_and_restart(): 66 | updater.stop() 67 | os.execl(sys.executable, sys.executable, *sys.argv) 68 | 69 | def restart(_): 70 | Thread(target=stop_and_restart).start() 71 | 72 | job_queue = updater.job_queue 73 | job_queue.run_repeating(restart, interval=dt.timedelta(minutes=30)) 74 | 75 | # Get the dispatcher to register handlers 76 | dispatcher = updater.dispatcher 77 | 78 | # General commands handlers 79 | dispatcher.add_handler( 80 | CommandHandler( 81 | "start", send_support_options, Filters.regex("support"), run_async=True 82 | ) 83 | ) 84 | dispatcher.add_handler(CommandHandler("start", start_msg, run_async=True)) 85 | 86 | dispatcher.add_handler(CommandHandler("help", help_msg, run_async=True)) 87 | dispatcher.add_handler(CommandHandler("setlang", send_lang, run_async=True)) 88 | dispatcher.add_handler( 89 | CommandHandler("support", send_support_options, run_async=True) 90 | ) 91 | 92 | # Callback query handler 93 | dispatcher.add_handler(CallbackQueryHandler(process_callback_query, run_async=True)) 94 | 95 | # Payment handlers 96 | dispatcher.add_handler(PreCheckoutQueryHandler(precheckout_check, run_async=True)) 97 | dispatcher.add_handler( 98 | MessageHandler(Filters.successful_payment, successful_payment, run_async=True) 99 | ) 100 | 101 | # URL handler 102 | dispatcher.add_handler( 103 | MessageHandler(Filters.entity(MessageEntity.URL), url_to_pdf, run_async=True) 104 | ) 105 | 106 | # PDF commands handlers 107 | dispatcher.add_handler(compare_cov_handler()) 108 | dispatcher.add_handler(merge_cov_handler()) 109 | dispatcher.add_handler(photo_cov_handler()) 110 | dispatcher.add_handler(text_cov_handler()) 111 | dispatcher.add_handler(watermark_cov_handler()) 112 | 113 | # PDF file handler 114 | dispatcher.add_handler(file_cov_handler()) 115 | 116 | # Feedback handler 117 | dispatcher.add_handler(feedback_cov_handler()) 118 | 119 | # Dev commands handlers 120 | dispatcher.add_handler(CommandHandler("send", send_msg, Filters.user(DEV_TELE_ID))) 121 | dispatcher.add_handler( 122 | CommandHandler("stats", get_stats, Filters.user(DEV_TELE_ID)) 123 | ) 124 | 125 | # Log all errors 126 | dispatcher.add_error_handler(error_callback) 127 | 128 | # Start the Bot 129 | updater.start_polling() 130 | log.notice("Bot started polling") 131 | 132 | # Run the bot until the you presses Ctrl-C or the process receives SIGINT, 133 | # SIGTERM or SIGABRT. This should be used most of the time, since 134 | # start_polling() is non-blocking and will stop the bot gracefully. 135 | updater.idle() 136 | 137 | 138 | def start_msg(update: Update, context: CallbackContext) -> None: 139 | update.effective_message.reply_chat_action(ChatAction.TYPING) 140 | 141 | # Create the user entity in Datastore 142 | create_user(update.effective_message.from_user) 143 | 144 | _ = set_lang(update, context) 145 | update.effective_message.reply_text( 146 | _( 147 | "Welcome to PDF Bot!\n\nKey features:\n" 148 | "- Compress, merge, preview, rename, split and add watermark to PDF files\n" 149 | "- Create PDF files from text messages\n" 150 | "- Extract images and text from PDF files\n" 151 | "- Convert PDF files into images\n" 152 | "- Convert webpages and images into PDF files\n" 153 | "- Beautify handwritten notes images into PDF files\n" 154 | "- And more...\n\n" 155 | "Type /help to see how to use PDF Bot" 156 | ), 157 | parse_mode=ParseMode.HTML, 158 | ) 159 | 160 | 161 | def help_msg(update, context): 162 | update.effective_message.reply_chat_action(ChatAction.TYPING) 163 | _ = set_lang(update, context) 164 | keyboard = [ 165 | [InlineKeyboardButton(_("Set Language 🌎"), callback_data=SET_LANG)], 166 | [ 167 | InlineKeyboardButton(_("Join Channel"), f"https://t.me/{CHANNEL_NAME}"), 168 | InlineKeyboardButton(_("Support PDF Bot"), callback_data=PAYMENT), 169 | ], 170 | ] 171 | reply_markup = InlineKeyboardMarkup(keyboard) 172 | 173 | update.effective_message.reply_text( 174 | _( 175 | "You can perform most of the tasks by sending me one of the followings:\n" 176 | "- PDF files\n- Photos\n- Webpage links\n\n" 177 | "The rest of the tasks can be performed by using the commands below:\n" 178 | "/compare - compare PDF files\n" 179 | "/merge - merge PDF files\n" 180 | "/photo - convert and combine multiple photos into PDF files\n" 181 | "/text - create PDF files from text messages\n" 182 | "/watermark - add watermark to PDF files" 183 | ), 184 | reply_markup=reply_markup, 185 | ) 186 | 187 | 188 | def process_callback_query(update: Update, context: CallbackContext): 189 | _ = set_lang(update, context) 190 | query = update.callback_query 191 | data = query.data 192 | 193 | if CALLBACK_DATA not in context.user_data: 194 | context.user_data[CALLBACK_DATA] = set() 195 | 196 | if data not in context.user_data[CALLBACK_DATA]: 197 | context.user_data[CALLBACK_DATA].add(data) 198 | if data == SET_LANG: 199 | send_lang(update, context, query) 200 | elif data in LANGUAGES: 201 | store_lang(update, context, query) 202 | if data == PAYMENT: 203 | send_support_options(update, context, query) 204 | elif data in [THANKS, COFFEE, BEER, MEAL]: 205 | send_payment_invoice(update, context, query) 206 | 207 | context.user_data[CALLBACK_DATA].remove(data) 208 | 209 | query.answer() 210 | 211 | 212 | def send_msg(update: Update, context: CallbackContext): 213 | tele_id = int(context.args[0]) 214 | message = " ".join(context.args[1:]) 215 | 216 | try: 217 | context.bot.send_message(tele_id, message) 218 | update.effective_message.reply_text("Message sent") 219 | except Exception as e: 220 | log = Logger() 221 | log.error(e) 222 | update.effective_message.reply_text("Failed to send message") 223 | 224 | 225 | def error_callback(update: Update, context: CallbackContext): 226 | if context.error is not Unauthorized: 227 | log = Logger() 228 | log.error(f'Update "{update}" caused error "{context.error}"') 229 | 230 | 231 | if __name__ == "__main__": 232 | main() 233 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | project_id_env: CROWDIN_PROJECT_ID 2 | api_token_env: CROWDIN_PERSONAL_TOKEN 3 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git add . 3 | git commit -am "make it better" 4 | git push -u heroku master 5 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | worker: Dockerfile 4 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrBotDeveloper/PDF-Bot/e6f9953529cd7e89eea4ae2c9aa2bcbef51d387a/images/icon.png -------------------------------------------------------------------------------- /images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 98 | -------------------------------------------------------------------------------- /images/icon_beta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrBotDeveloper/PDF-Bot/e6f9953529cd7e89eea4ae2c9aa2bcbef51d387a/images/icon_beta.png -------------------------------------------------------------------------------- /images/icon_dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrBotDeveloper/PDF-Bot/e6f9953529cd7e89eea4ae2c9aa2bcbef51d387a/images/icon_dev.png -------------------------------------------------------------------------------- /input.txt: -------------------------------------------------------------------------------- 1 | ! TRAVIS input file 2 | ! Created with TRAVIS version compiled at Mar 22 2020 16:01:08 3 | ! Source code version: Jan 01 2019 4 | ! Input file written at Fri May 21 13:02:16 2021. 5 | -------------------------------------------------------------------------------- /locale/ky_KG/LC_MESSAGES/pdf_bot.po: -------------------------------------------------------------------------------- 1 | # locale translations for telegram-pdf-bot. 2 | # Copyright (C) 2021 zeshuaro 3 | # This file is distributed under the same license as the telegram-pdf-bot 4 | # project. 5 | # zeshuaro , 2021. 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: telegram-pdf-bot\n" 9 | "Report-Msgid-Bugs-To: zeshuaro@gmail.com\n" 10 | "POT-Creation-Date: 2021-01-21 07:49+0000\n" 11 | "PO-Revision-Date: 2021-01-21 08:50\n" 12 | "Last-Translator: \n" 13 | "Language: ky\n" 14 | "Language-Team: Kyrgyz\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 2.9.0\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | "X-Crowdin-Project: telegram-pdf-bot\n" 21 | "X-Crowdin-Project-ID: 370289\n" 22 | "X-Crowdin-Language: ky\n" 23 | "X-Crowdin-File: /master/locale/en_GB/LC_MESSAGES/pdf_bot.po\n" 24 | "X-Crowdin-File-ID: 88\n" 25 | 26 | #: bot.py:149 27 | msgid "Welcome to PDF Bot!\n\n" 28 | "Key features:\n" 29 | "- Compress, merge, preview, rename, split and add watermark to PDF files\n" 30 | "- Create PDF files from text messages\n" 31 | "- Extract images and text from PDF files\n" 32 | "- Convert PDF files into images\n" 33 | "- Convert webpages and images into PDF files\n" 34 | "- Beautify handwritten notes images into PDF files\n" 35 | "- And more...\n\n" 36 | "Type /help to see how to use PDF Bot" 37 | msgstr "" 38 | 39 | #: bot.py:168 40 | msgid "Set Language 🌎" 41 | msgstr "" 42 | 43 | #: bot.py:170 pdf_bot/utils.py:307 44 | msgid "Join Channel" 45 | msgstr "" 46 | 47 | #: bot.py:171 pdf_bot/payment.py:78 pdf_bot/utils.py:308 48 | msgid "Support PDF Bot" 49 | msgstr "" 50 | 51 | #: bot.py:177 52 | msgid "You can perform most of the tasks by sending me one of the followings:\n" 53 | "- PDF files\n" 54 | "- Photos\n" 55 | "- Webpage links\n\n" 56 | "The rest of the tasks can be performed by using the commands below:\n" 57 | "/compare - compare PDF files\n" 58 | "/merge - merge PDF files\n" 59 | "/photo - convert and combine multiple photos into PDF files\n" 60 | "/text - create PDF files from text messages\n" 61 | "/watermark - add watermark to PDF files" 62 | msgstr "" 63 | 64 | #: bot.py:212 pdf_bot/constants.py:84 65 | msgid "Send me the amount that you'll like to support PDF Bot" 66 | msgstr "" 67 | 68 | #: pdf_bot/constants.py:38 69 | msgid "Cancel" 70 | msgstr "Жокко чыгарылды" 71 | 72 | #: pdf_bot/constants.py:39 73 | msgid "Done" 74 | msgstr "Даяр" 75 | 76 | #: pdf_bot/constants.py:40 77 | msgid "Back" 78 | msgstr "Артка" 79 | 80 | #: pdf_bot/constants.py:41 81 | msgid "By Percentage" 82 | msgstr "Пайыз менен" 83 | 84 | #: pdf_bot/constants.py:42 85 | msgid "By Margin Size" 86 | msgstr "Өлчөмү боюнча" 87 | 88 | #: pdf_bot/constants.py:43 89 | msgid "Preview" 90 | msgstr "Алдын ала көрүү" 91 | 92 | #: pdf_bot/constants.py:44 93 | msgid "Decrypt" 94 | msgstr "" 95 | 96 | #: pdf_bot/constants.py:45 97 | msgid "Encrypt" 98 | msgstr "" 99 | 100 | #: pdf_bot/constants.py:46 101 | msgid "Extract Photos" 102 | msgstr "" 103 | 104 | #: pdf_bot/constants.py:47 105 | msgid "To Photos" 106 | msgstr "Сүрөттөргө" 107 | 108 | #: pdf_bot/constants.py:48 109 | msgid "Rotate" 110 | msgstr "" 111 | 112 | #: pdf_bot/constants.py:49 113 | msgid "Scale" 114 | msgstr "Өлчөм" 115 | 116 | #: pdf_bot/constants.py:50 117 | msgid "Split" 118 | msgstr "Ажыратуу" 119 | 120 | #: pdf_bot/constants.py:51 121 | msgid "Beautify" 122 | msgstr "" 123 | 124 | #: pdf_bot/constants.py:52 125 | msgid "To PDF" 126 | msgstr "" 127 | 128 | #: pdf_bot/constants.py:53 129 | msgid "Rename" 130 | msgstr "Өзгөртүү" 131 | 132 | #: pdf_bot/constants.py:54 133 | msgid "Crop" 134 | msgstr "Кесүү" 135 | 136 | #: pdf_bot/constants.py:55 137 | msgid "Compressed" 138 | msgstr "" 139 | 140 | #: pdf_bot/constants.py:56 141 | msgid "Photos" 142 | msgstr "Сүрөттөр" 143 | 144 | #: pdf_bot/constants.py:57 145 | msgid "Remove Last File" 146 | msgstr "Акыркы файлды өчүрүү" 147 | 148 | #: pdf_bot/constants.py:58 149 | msgid "To Dimensions" 150 | msgstr "" 151 | 152 | #: pdf_bot/constants.py:59 153 | msgid "Extract Text" 154 | msgstr "" 155 | 156 | #: pdf_bot/constants.py:60 157 | msgid "Text Message" 158 | msgstr "" 159 | 160 | #: pdf_bot/constants.py:61 161 | msgid "Text File" 162 | msgstr "" 163 | 164 | #: pdf_bot/constants.py:63 165 | msgid "Compress" 166 | msgstr "" 167 | 168 | #: pdf_bot/constants.py:78 169 | msgid "Say Thanks 😁 ($1)" 170 | msgstr "Рахмат 😁 ($1)" 171 | 172 | #: pdf_bot/constants.py:79 173 | msgid "Coffee ☕ ($3)" 174 | msgstr "Кофе ☕ ($3)" 175 | 176 | #: pdf_bot/constants.py:80 177 | msgid "Beer 🍺 ($5)" 178 | msgstr "Пиво 🍺 ($5)" 179 | 180 | #: pdf_bot/constants.py:81 181 | msgid "Meal 🍲 ($10)" 182 | msgstr "Тамактануу 🍲 ($10)" 183 | 184 | #: pdf_bot/constants.py:82 185 | msgid "Say Awesome 🤩 (Custom)" 186 | msgstr "Мыкты деңиз 🤩 (жеке)" 187 | 188 | #: pdf_bot/feedback.py:46 189 | msgid "Send me your feedback (only English feedback will be forwarded to my developer)" 190 | msgstr "" 191 | 192 | #: pdf_bot/feedback.py:79 193 | msgid "The feedback is not in English, try again" 194 | msgstr "" 195 | 196 | #: pdf_bot/feedback.py:99 197 | msgid "Thank you for your feedback, I've already forwarded it to my developer" 198 | msgstr "" 199 | 200 | #: pdf_bot/language.py:24 201 | msgid "Select your language" 202 | msgstr "" 203 | 204 | #: pdf_bot/language.py:62 205 | msgid "Your language has been set to {}" 206 | msgstr "" 207 | 208 | #: pdf_bot/payment.py:32 209 | msgid "The amount you sent is invalid, try again. {}" 210 | msgstr "" 211 | 212 | #: pdf_bot/payment.py:53 213 | msgid "Help translate PDF Bot" 214 | msgstr "" 215 | 216 | #: pdf_bot/payment.py:58 217 | msgid "Select how you want to support PDF Bot" 218 | msgstr "" 219 | 220 | #: pdf_bot/payment.py:79 221 | msgid "Say thanks to PDF Bot and help keep it running" 222 | msgstr "" 223 | 224 | #: pdf_bot/payment.py:106 225 | msgid "Something went wrong" 226 | msgstr "" 227 | 228 | #: pdf_bot/payment.py:114 229 | msgid "Thank you for your support!" 230 | msgstr "" 231 | 232 | #: pdf_bot/url.py:24 233 | msgid "You've sent me this web page already and I'm still converting it" 234 | msgstr "" 235 | 236 | #: pdf_bot/url.py:27 237 | msgid "Converting your web page into a PDF file" 238 | msgstr "" 239 | 240 | #: pdf_bot/url.py:39 241 | msgid "Unable to reach your web page" 242 | msgstr "" 243 | 244 | #: pdf_bot/utils.py:34 245 | msgid "Action cancelled" 246 | msgstr "" 247 | 248 | #: pdf_bot/utils.py:67 249 | msgid "The file you sent is not a PDF file, try again" 250 | msgstr "" 251 | 252 | #: pdf_bot/utils.py:72 253 | msgid "The PDF file you sent is too large for me to download\n\n" 254 | "I've cancelled your action" 255 | msgstr "" 256 | 257 | #: pdf_bot/utils.py:101 258 | msgid "Something went wrong, start over again" 259 | msgstr "" 260 | 261 | #: pdf_bot/utils.py:188 262 | msgid "Your PDF file seems to be invalid and I couldn't open and read it\n\n" 263 | "I've cancelled your action" 264 | msgstr "" 265 | 266 | #: pdf_bot/utils.py:197 267 | msgid "Your PDF file is already encrypted" 268 | msgstr "" 269 | 270 | #: pdf_bot/utils.py:199 271 | msgid "Your {} PDF file is encrypted and you'll have to decrypt it first\n\n" 272 | "I've cancelled your action" 273 | msgstr "" 274 | 275 | #: pdf_bot/utils.py:204 276 | msgid "Your PDF file is encrypted and you'll have to decrypt it first\n\n" 277 | "I've cancelled your action" 278 | msgstr "" 279 | 280 | #: pdf_bot/utils.py:228 281 | msgid "You've sent me these {} so far:\n" 282 | msgstr "" 283 | 284 | #: pdf_bot/utils.py:276 285 | msgid "The result file is too large for me to send to you" 286 | msgstr "" 287 | 288 | #: pdf_bot/utils.py:284 pdf_bot/utils.py:291 289 | msgid "Here is your result file" 290 | msgstr "" 291 | 292 | #: pdf_bot/commands/compare.py:54 293 | msgid "Send me one of the PDF files that you'll like to compare\n\n" 294 | "Note that I can only look for differences in text" 295 | msgstr "" 296 | 297 | #: pdf_bot/commands/compare.py:88 298 | msgid "Send me the other PDF file that you'll like to compare" 299 | msgstr "" 300 | 301 | #: pdf_bot/commands/compare.py:112 302 | msgid "Comparing your PDF files" 303 | msgstr "" 304 | 305 | #: pdf_bot/commands/compare.py:131 306 | msgid "There are no differences in text between your PDF files" 307 | msgstr "" 308 | 309 | #: pdf_bot/commands/merge.py:77 310 | msgid "Send me the PDF files that you'll like to merge\n\n" 311 | "Note that the files will be merged in the order that you send me" 312 | msgstr "" 313 | 314 | #: pdf_bot/commands/merge.py:110 315 | msgid "The file you've sent is not a PDF file" 316 | msgstr "" 317 | 318 | #: pdf_bot/commands/merge.py:112 319 | msgid "The PDF file you've sent is too large for me to download" 320 | msgstr "" 321 | 322 | #: pdf_bot/commands/merge.py:130 323 | msgid "PDF files" 324 | msgstr "" 325 | 326 | #: pdf_bot/commands/merge.py:137 327 | msgid "Press Done if you've sent me all the PDF files that you'll like to merge or keep sending me the PDF files" 328 | msgstr "" 329 | 330 | #: pdf_bot/commands/merge.py:178 331 | msgid "{} has been removed for merging" 332 | msgstr "" 333 | 334 | #: pdf_bot/commands/merge.py:198 335 | msgid "You haven't sent me any PDF files" 336 | msgstr "" 337 | 338 | #: pdf_bot/commands/merge.py:202 339 | msgid "You've only sent me one PDF file." 340 | msgstr "" 341 | 342 | #: pdf_bot/commands/merge.py:216 343 | msgid "Merging your PDF files" 344 | msgstr "" 345 | 346 | #: pdf_bot/commands/merge.py:236 347 | msgid "I can't merge your PDF files as I couldn't open and read \"{}\". Ensure that it is not encrypted" 348 | msgstr "" 349 | 350 | #: pdf_bot/commands/photo.py:72 351 | msgid "Send me the photos that you'll like to beautify or convert into a PDF file\n\n" 352 | "Note that the photos will be beautified and converted in the order that you send me" 353 | msgstr "" 354 | 355 | #: pdf_bot/commands/photo.py:99 356 | msgid "File name unavailable" 357 | msgstr "" 358 | 359 | #: pdf_bot/commands/photo.py:118 360 | msgid "The file you've sent is not a photo" 361 | msgstr "" 362 | 363 | #: pdf_bot/commands/photo.py:124 364 | msgid "The photo you've sent is too large for me to download" 365 | msgstr "" 366 | 367 | #: pdf_bot/commands/photo.py:131 368 | msgid "photos" 369 | msgstr "сүрөттөр" 370 | 371 | #: pdf_bot/commands/photo.py:138 372 | msgid "Select the task from below if you've sent me all the photos, or keep sending me the photos" 373 | msgstr "" 374 | 375 | #: pdf_bot/commands/photo.py:182 376 | msgid "{} has been removed for beautifying or converting" 377 | msgstr "" 378 | 379 | #: pdf_bot/commands/photo.py:217 380 | msgid "Beautifying and converting your photos" 381 | msgstr "" 382 | 383 | #: pdf_bot/commands/photo.py:222 384 | msgid "Converting your photos into PDF" 385 | msgstr "" 386 | 387 | #: pdf_bot/commands/text.py:45 388 | msgid "Send me the text that you'll like to write into your PDF file" 389 | msgstr "" 390 | 391 | #: pdf_bot/commands/text.py:68 392 | msgid "Creating your PDF file" 393 | msgstr "" 394 | 395 | #: pdf_bot/commands/watermark.py:49 396 | msgid "Send me the PDF file that you'll like to add a watermark" 397 | msgstr "" 398 | 399 | #: pdf_bot/commands/watermark.py:80 400 | msgid "Send me the watermark PDF file" 401 | msgstr "" 402 | 403 | #: pdf_bot/commands/watermark.py:105 404 | msgid "Adding the watermark onto your PDF file" 405 | msgstr "" 406 | 407 | #: pdf_bot/commands/watermark.py:118 408 | msgid "watermark" 409 | msgstr "көрүнбөс белги" 410 | 411 | #: pdf_bot/files/compress.py:20 412 | msgid "Compressing your PDF file" 413 | msgstr "" 414 | 415 | #: pdf_bot/files/compress.py:49 416 | msgid "File size reduced by {:.0%}, from {} to {}" 417 | msgstr "" 418 | 419 | #: pdf_bot/files/compress.py:58 pdf_bot/files/crop.py:130 420 | #: pdf_bot/files/photo.py:218 421 | msgid "Something went wrong, try again" 422 | msgstr "" 423 | 424 | #: pdf_bot/files/crop.py:31 425 | msgid "Select the crop type that you'll like to perform" 426 | msgstr "" 427 | 428 | #: pdf_bot/files/crop.py:46 429 | msgid "Send me a number between {} and {}. This is the percentage of margin space to retain between the content in your PDF file and the page" 430 | msgstr "" 431 | 432 | #: pdf_bot/files/crop.py:56 433 | msgid "Send me a number that you'll like to adjust the margin size. Positive numbers will decrease the margin size and negative numbers will increase it" 434 | msgstr "" 435 | 436 | #: pdf_bot/files/crop.py:77 437 | msgid "The number must be between {} and {}, try again" 438 | msgstr "" 439 | 440 | #: pdf_bot/files/crop.py:98 441 | msgid "The number is invalid, try again" 442 | msgstr "" 443 | 444 | #: pdf_bot/files/crop.py:108 445 | msgid "Cropping your PDF file" 446 | msgstr "" 447 | 448 | #: pdf_bot/files/crypto.py:17 449 | msgid "Send me the password to decrypt your PDF file" 450 | msgstr "" 451 | 452 | #: pdf_bot/files/crypto.py:32 453 | msgid "Decrypting your PDF file" 454 | msgstr "" 455 | 456 | #: pdf_bot/files/crypto.py:47 457 | msgid "Your PDF file seems to be invalid and I couldn't open and read it" 458 | msgstr "" 459 | 460 | #: pdf_bot/files/crypto.py:52 461 | msgid "Your PDF file is not encrypted" 462 | msgstr "" 463 | 464 | #: pdf_bot/files/crypto.py:57 465 | msgid "The decryption password is incorrect, try to send it again" 466 | msgstr "" 467 | 468 | #: pdf_bot/files/crypto.py:71 469 | msgid "Your PDF file is encrypted with a method that I cannot decrypt" 470 | msgstr "" 471 | 472 | #: pdf_bot/files/crypto.py:86 473 | msgid "Send me the password to encrypt your PDF file" 474 | msgstr "" 475 | 476 | #: pdf_bot/files/crypto.py:100 477 | msgid "Encrypting your PDF file" 478 | msgstr "" 479 | 480 | #: pdf_bot/files/document.py:52 pdf_bot/files/photo.py:64 481 | msgid "Select the task that you'll like to perform" 482 | msgstr "" 483 | 484 | #: pdf_bot/files/file.py:106 485 | msgid "Your PDF file is too big for me to download\n\n" 486 | "I can't perform any tasks on it" 487 | msgstr "" 488 | 489 | #: pdf_bot/files/ocr.py:20 490 | msgid "Adding an OCR text layer to your PDF file" 491 | msgstr "" 492 | 493 | #: pdf_bot/files/ocr.py:38 494 | msgid "Your PDF file already has a text layer" 495 | msgstr "" 496 | 497 | #: pdf_bot/files/photo.py:50 498 | msgid "Your photo is too large for me to download. I can't beautify or convert your photo" 499 | msgstr "" 500 | 501 | #: pdf_bot/files/photo.py:105 502 | msgid "Extracting a preview for your PDF file" 503 | msgstr "" 504 | 505 | #: pdf_bot/files/photo.py:153 506 | msgid "Select the result file format" 507 | msgstr "" 508 | 509 | #: pdf_bot/files/photo.py:165 510 | msgid "Converting your PDF file into photos" 511 | msgstr "" 512 | 513 | #: pdf_bot/files/photo.py:203 514 | msgid "Extracting all the photos in your PDF file" 515 | msgstr "" 516 | 517 | #: pdf_bot/files/photo.py:223 518 | msgid "I couldn't find any photos in your PDF file" 519 | msgstr "" 520 | 521 | #: pdf_bot/files/photo.py:259 522 | msgid "See above for all your photos" 523 | msgstr "" 524 | 525 | #: pdf_bot/files/rename.py:19 526 | msgid "Send me the file name that you'll like to rename your PDF file into" 527 | msgstr "" 528 | 529 | #: pdf_bot/files/rename.py:38 530 | msgid "File names can't contain any of the following characters:\n" 531 | "{}\n" 532 | "Send me another file name" 533 | msgstr "" 534 | 535 | #: pdf_bot/files/rename.py:48 536 | msgid "Renaming your PDF file into {}" 537 | msgstr "" 538 | 539 | #: pdf_bot/files/rotate.py:24 540 | msgid "Select the degrees that you'll like to rotate your PDF file in clockwise" 541 | msgstr "" 542 | 543 | #: pdf_bot/files/rotate.py:48 544 | msgid "Rotating your PDF file clockwise by {} degrees" 545 | msgstr "" 546 | 547 | #: pdf_bot/files/scale.py:24 548 | msgid "Select the scale type that you'll like to perform" 549 | msgstr "" 550 | 551 | #: pdf_bot/files/scale.py:38 552 | msgid "Send me the width and height\n\n" 553 | "Example: 150 200 (this will set the width to 150 and height to 200)" 554 | msgstr "" 555 | 556 | #: pdf_bot/files/scale.py:49 557 | msgid "Send me the scaling factors for the horizontal and vertical axes\n\n" 558 | "Example: 2 0.5 (this will double the horizontal axis and halve the vertical axis)" 559 | msgstr "" 560 | 561 | #: pdf_bot/files/scale.py:73 562 | msgid "The scaling factors {} are invalid, try again" 563 | msgstr "" 564 | 565 | #: pdf_bot/files/scale.py:93 566 | msgid "The dimensions {} are invalid, try again" 567 | msgstr "" 568 | 569 | #: pdf_bot/files/scale.py:105 570 | msgid "Scaling your PDF file, horizontally by {} and vertically by {}" 571 | msgstr "" 572 | 573 | #: pdf_bot/files/scale.py:115 574 | msgid "Scaling your PDF file with width of {} and height of {}" 575 | msgstr "" 576 | 577 | #: pdf_bot/files/split.py:36 578 | msgid "Send me the range of pages that you'll like to keep" 579 | msgstr "" 580 | 581 | #: pdf_bot/files/split.py:37 582 | msgid "General usage" 583 | msgstr "" 584 | 585 | #: pdf_bot/files/split.py:38 586 | msgid "all pages" 587 | msgstr "" 588 | 589 | #: pdf_bot/files/split.py:39 590 | msgid "page 8 only" 591 | msgstr "" 592 | 593 | #: pdf_bot/files/split.py:40 594 | msgid "first three pages" 595 | msgstr "" 596 | 597 | #: pdf_bot/files/split.py:41 598 | msgid "from page 8 onward" 599 | msgstr "" 600 | 601 | #: pdf_bot/files/split.py:42 602 | msgid "last page only" 603 | msgstr "" 604 | 605 | #: pdf_bot/files/split.py:43 606 | msgid "all pages except the last page" 607 | msgstr "" 608 | 609 | #: pdf_bot/files/split.py:44 610 | msgid "second last page only" 611 | msgstr "" 612 | 613 | #: pdf_bot/files/split.py:45 614 | msgid "last two pages" 615 | msgstr "" 616 | 617 | #: pdf_bot/files/split.py:46 618 | msgid "third and second last pages" 619 | msgstr "" 620 | 621 | #: pdf_bot/files/split.py:47 622 | msgid "Advanced usage" 623 | msgstr "" 624 | 625 | #: pdf_bot/files/split.py:48 626 | msgid "pages" 627 | msgstr "" 628 | 629 | #: pdf_bot/files/split.py:49 630 | msgid "to the end" 631 | msgstr "" 632 | 633 | #: pdf_bot/files/split.py:50 634 | msgid "all pages in reversed order" 635 | msgstr "" 636 | 637 | #: pdf_bot/files/split.py:51 638 | msgid "except" 639 | msgstr "" 640 | 641 | #: pdf_bot/files/split.py:75 642 | msgid "The range is invalid. Try again" 643 | msgstr "" 644 | 645 | #: pdf_bot/files/split.py:81 646 | msgid "Splitting your PDF file" 647 | msgstr "" 648 | 649 | #: pdf_bot/files/text.py:22 650 | msgid "Select how you'll like me to send the text to you" 651 | msgstr "" 652 | 653 | #: pdf_bot/files/text.py:35 654 | msgid "Extracting text from your PDF file" 655 | msgstr "" 656 | 657 | #: pdf_bot/files/text.py:84 658 | msgid "See above for all the text in your PDF file" 659 | msgstr "" 660 | 661 | #: pdf_bot/files/text.py:88 662 | msgid "I couldn't find any text in your PDF file" 663 | msgstr "" 664 | 665 | #~ msgid "" 666 | #~ "Send me the first photo that " 667 | #~ "you'll like to beautify or convert " 668 | #~ "into PDF\n" 669 | #~ "\n" 670 | #~ "Note that the photos will be " 671 | #~ "beautified and converted in the order" 672 | #~ " that you send me" 673 | #~ msgstr "" 674 | 675 | #~ msgid "" 676 | #~ "Send me the next photo that you'll like to beautify or convert to PDF\n" 677 | #~ "\n" 678 | #~ "Select the task from below if you have sent me all the photos" 679 | #~ msgstr "" 680 | 681 | #~ msgid "" 682 | #~ "Welcome to PDF Bot!\n" 683 | #~ "\n" 684 | #~ "*Features*\n" 685 | #~ "- Compare, crop, decrypt, encrypt, " 686 | #~ "merge, rotate, scale, split and add " 687 | #~ "a watermark to a PDF file\n" 688 | #~ "- Extract text and photos in a " 689 | #~ "PDF file and convert a PDF file" 690 | #~ " into photos\n" 691 | #~ "- Beautify and convert photos into PDF format\n" 692 | #~ "- Convert a web page into a PDF file\n" 693 | #~ "\n" 694 | #~ "Type /help to see how to use PDF Bot" 695 | #~ msgstr "" 696 | 697 | #~ msgid "" 698 | #~ "You can perform most of the tasks" 699 | #~ " simply by sending me a PDF " 700 | #~ "file, a photo or a link to a" 701 | #~ " web page.\n" 702 | #~ "\n" 703 | #~ "Some tasks can be performed by " 704 | #~ "using the commands /compare, /merge, " 705 | #~ "/watermark or /photo" 706 | #~ msgstr "" 707 | 708 | #~ msgid "" 709 | #~ "You can perform most of the tasks" 710 | #~ " by sending me one of the " 711 | #~ "followings:\n" 712 | #~ "- PDF files\n" 713 | #~ "- Photos\n" 714 | #~ "- Webpage links\n" 715 | #~ "\n" 716 | #~ "The rest of the tasks can be " 717 | #~ "performed by using the commands " 718 | #~ "/compare, /merge, /photo, /text or " 719 | #~ "/watermark" 720 | #~ msgstr "" 721 | 722 | #~ msgid "" 723 | #~ "Welcome to PDF Bot!\n" 724 | #~ "\n" 725 | #~ "*Key features:*\n" 726 | #~ "- Compress, merge, preview, rename, " 727 | #~ "split and add watermark to PDF " 728 | #~ "files\n" 729 | #~ "- Create PDF files from text messages\n" 730 | #~ "- Extract images and text from PDF files\n" 731 | #~ "- Convert PDF files into images\n" 732 | #~ "- Convert webpages and images into PDF files\n" 733 | #~ "- Beautify handwritten notes images into PDF files\n" 734 | #~ "- _And more..._\n" 735 | #~ "\n" 736 | #~ "Type /help to see how to use PDF Bot" 737 | #~ msgstr "" 738 | 739 | #~ msgid "" 740 | #~ "You can perform most of the tasks" 741 | #~ " by sending me one of the " 742 | #~ "followings:\n" 743 | #~ "- PDF files\n" 744 | #~ "- Photos\n" 745 | #~ "- Webpage links\n" 746 | #~ "\n" 747 | #~ "The rest of the tasks can be performed by using the commands below:\n" 748 | #~ "- /compare _PDF files_\n" 749 | #~ "- /merge _PDF files_\n" 750 | #~ "- /photo _convert and combine multiple photos into PDF files_\n" 751 | #~ "- /text _create PDF files from text messages_\n" 752 | #~ "- /watermark _add watermark to PDF files_" 753 | #~ msgstr "" 754 | 755 | #~ msgid "" 756 | #~ "Press *Done* if you've sent me all" 757 | #~ " the PDF files that you'll like " 758 | #~ "to merge or keep sending me the" 759 | #~ " PDF files" 760 | #~ msgstr "" 761 | 762 | #~ msgid "*{}* has been removed for merging" 763 | #~ msgstr "" 764 | 765 | #~ msgid "*{}* has been removed for beautifying or converting" 766 | #~ msgstr "" 767 | 768 | #~ msgid "File size reduced by *{:.0%}*, from *{}* to *{}*" 769 | #~ msgstr "" 770 | 771 | #~ msgid "Renaming your PDF file into *{}*" 772 | #~ msgstr "" 773 | 774 | #~ msgid "" 775 | #~ "Send me the width and height\n" 776 | #~ "\n" 777 | #~ "*Example: 150 200* (this will set the width to 150 and height to 200)" 778 | #~ msgstr "" 779 | 780 | #~ msgid "" 781 | #~ "Send me the scaling factors for the horizontal and vertical axes\n" 782 | #~ "\n" 783 | #~ "2 will double the axis and 0.5 will halve the axis\n" 784 | #~ "\n" 785 | #~ "*Example: 2 0.5* (this will double " 786 | #~ "the horizontal axis and halve the " 787 | #~ "vertical axis)" 788 | #~ msgstr "" 789 | 790 | #~ msgid "The scaling factors *{}* are invalid, try again" 791 | #~ msgstr "" 792 | 793 | #~ msgid "The dimensions *{}* are invalid, try again" 794 | #~ msgstr "" 795 | 796 | #~ msgid "Scaling your PDF file, horizontally by *{}* and vertically by *{}*" 797 | #~ msgstr "" 798 | 799 | #~ msgid "Scaling your PDF file with width of *{}* and height of *{}*" 800 | #~ msgstr "" 801 | 802 | #~ msgid "" 803 | #~ "Send me the range of pages that" 804 | #~ " you'll like to keep. Use ⚡ " 805 | #~ "*INSTANT VIEW* from below or refer " 806 | #~ "to [here](http://telegra.ph/Telegram-PDF-Bot-07-16)" 807 | #~ " for some range examples." 808 | #~ msgstr "" 809 | 810 | #~ msgid "*See above for all the text in your PDF file*" 811 | #~ msgstr "" 812 | 813 | #~ msgid "" 814 | #~ "Send me the range of pages that" 815 | #~ " you'll like to keep. Use ⚡ " 816 | #~ "INSTANT VIEW from below or refer" 817 | #~ " to [here](http://telegra.ph/Telegram-PDF-" 818 | #~ "Bot-07-16) for some range examples." 819 | #~ msgstr "" 820 | 821 | #~ msgid "" 822 | #~ "Send me your feedback or /cancel " 823 | #~ "this action. Note that only English " 824 | #~ "feedback will be forwarded to my " 825 | #~ "developer." 826 | #~ msgstr "" 827 | 828 | #~ msgid "" 829 | #~ "Send me your feedback, note that " 830 | #~ "only English feedback will be forwarded" 831 | #~ " to my developer" 832 | #~ msgstr "" 833 | 834 | #~ msgid "" 835 | #~ "Send me the range of pages that you'll like to keep\n" 836 | #~ "\n" 837 | #~ "General usage\n" 838 | #~ ": all pages\n" 839 | #~ "22 just the 23rd page\n" 840 | #~ "0:3 the first three pages\n" 841 | #~ ":3 the first three pages\n" 842 | #~ "5: from the 6th page onwards\n" 843 | #~ "-1 last page only\n" 844 | #~ ":-1 all pages but the last page\n" 845 | #~ "-2 second last page only\n" 846 | #~ "-2: last two pages\n" 847 | #~ "-3:-1 third and second last pages only\n" 848 | #~ "\n" 849 | #~ "Advanced usage\n" 850 | #~ "::2 pages 0 2 4 ... to the end\n" 851 | #~ "1:10:2 pages 1 3 5 7 9\n" 852 | #~ "::-1 all pages in reversed order\n" 853 | #~ "3:0:-1 pages 3 2 1 but not 0\n" 854 | #~ "2::-1 pages 2 1 0" 855 | #~ msgstr "" 856 | 857 | #~ msgid "Set Language" 858 | #~ msgstr "" 859 | 860 | -------------------------------------------------------------------------------- /nltk.txt: -------------------------------------------------------------------------------- 1 | popular 2 | stopwords 3 | wordnet 4 | punkt 5 | pros_cons 6 | -------------------------------------------------------------------------------- /pdf-bot.metrics.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 |

11 | 12 | 13 | 14 | MrBotDeveloper/PDF-Bot 15 |

16 |
17 |
18 |
19 | 20 | 21 | 22 | Created 4 years ago 23 |
24 |
25 | 26 | 27 | 28 | Deployed 874 times 29 |
30 |
31 | 32 | 33 | 34 | 1.85 MB used 35 |
36 |
37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 |
59 | 60 | 61 | 62 | 3 Environments 63 |
64 |
65 | 66 | 67 | 68 | 0 added, 0 removed 69 |
70 |
71 |
72 |
73 |
74 |

75 | 76 | 77 | 78 | Overall issues and pull requests status 79 |

80 |
81 |
82 |
83 |
84 |

Issues

85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
100 |
101 | 102 | 103 | 104 | 105 | 1 open 106 |
107 |
108 | 109 | 110 | 111 | 112 | 6 closed 113 |
114 |
115 | 116 | 117 | 118 | 0 drafts 119 |
120 |
121 | 122 | 123 | 124 | 0 skipped 125 |
126 |
127 |
128 |
129 |

Pull requests

130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 |
145 |
146 | 147 | 148 | 149 | 5 open 150 |
151 |
152 | 153 | 154 | 155 | 244 merged 156 |
157 |
158 | 159 | 160 | 161 | 162 | 0 drafts 163 |
164 |
165 | 166 | 167 | 168 | 172 closed 169 |
170 |
171 |
172 |
173 |
174 |
175 | Last updated 1 Jun 2025, 01:47:19 with lowlighter/metrics@3.34.0 176 |
177 |
178 |
179 |
180 |
-------------------------------------------------------------------------------- /pdf_bot/__init__.py: -------------------------------------------------------------------------------- 1 | from pdf_bot.commands import ( 2 | compare_cov_handler, 3 | merge_cov_handler, 4 | photo_cov_handler, 5 | text_cov_handler, 6 | text_to_pdf, 7 | watermark_cov_handler, 8 | ) 9 | from pdf_bot.constants import * 10 | from pdf_bot.feedback import feedback_cov_handler 11 | from pdf_bot.files import file_cov_handler 12 | from pdf_bot.language import send_lang, set_lang, store_lang 13 | from pdf_bot.mq_bot import MQBot 14 | from pdf_bot.payment import ( 15 | precheckout_check, 16 | send_payment_invoice, 17 | send_support_options, 18 | successful_payment, 19 | ) 20 | from pdf_bot.stats import get_stats 21 | from pdf_bot.store import create_user 22 | from pdf_bot.url import url_to_pdf 23 | -------------------------------------------------------------------------------- /pdf_bot/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from pdf_bot.commands.compare import compare_cov_handler 2 | from pdf_bot.commands.merge import merge_cov_handler 3 | from pdf_bot.commands.photo import photo_cov_handler, process_photo 4 | from pdf_bot.commands.text import text_cov_handler, text_to_pdf 5 | from pdf_bot.commands.watermark import watermark_cov_handler 6 | -------------------------------------------------------------------------------- /pdf_bot/commands/compare.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | 4 | import pdf_diff 5 | from pdf_diff import NoDifferenceError 6 | from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove 7 | from telegram.ext import CommandHandler, ConversationHandler, Filters, MessageHandler 8 | 9 | from pdf_bot.constants import BACK, CANCEL, PDF_INVALID_FORMAT, PDF_OK, TEXT_FILTER 10 | from pdf_bot.language import set_lang 11 | from pdf_bot.utils import cancel, check_pdf, check_user_data, send_result_file 12 | 13 | WAIT_FIRST = 0 14 | WAIT_SECOND = 1 15 | COMPARE_ID = "compare_id" 16 | 17 | 18 | def compare_cov_handler(): 19 | conv_handler = ConversationHandler( 20 | entry_points=[CommandHandler("compare", compare, run_async=True)], 21 | states={ 22 | WAIT_FIRST: [ 23 | MessageHandler(Filters.document, check_first_doc, run_async=True) 24 | ], 25 | WAIT_SECOND: [ 26 | MessageHandler(Filters.document, check_second_doc, run_async=True) 27 | ], 28 | }, 29 | fallbacks=[ 30 | CommandHandler("cancel", cancel, run_async=True), 31 | MessageHandler(TEXT_FILTER, check_text, run_async=True), 32 | ], 33 | allow_reentry=True, 34 | ) 35 | 36 | return conv_handler 37 | 38 | 39 | def compare(update, context): 40 | return ask_first_doc(update, context) 41 | 42 | 43 | def ask_first_doc(update, context): 44 | _ = set_lang(update, context) 45 | reply_markup = ReplyKeyboardMarkup( 46 | [[_(CANCEL)]], resize_keyboard=True, one_time_keyboard=True 47 | ) 48 | update.effective_message.reply_text( 49 | _( 50 | "Send me one of the PDF files that you'll like to compare\n\n" 51 | "Note that I can only look for differences in text" 52 | ), 53 | reply_markup=reply_markup, 54 | ) 55 | 56 | return WAIT_FIRST 57 | 58 | 59 | def check_text(update, context): 60 | _ = set_lang(update, context) 61 | text = update.effective_message.text 62 | 63 | if text == _(BACK): 64 | return ask_first_doc(update, context) 65 | elif text == _(CANCEL): 66 | return cancel(update, context) 67 | 68 | 69 | def check_first_doc(update, context): 70 | result = check_pdf(update, context) 71 | if result == PDF_INVALID_FORMAT: 72 | return WAIT_FIRST 73 | elif result != PDF_OK: 74 | return ConversationHandler.END 75 | 76 | _ = set_lang(update, context) 77 | context.user_data[COMPARE_ID] = update.effective_message.document.file_id 78 | 79 | reply_markup = ReplyKeyboardMarkup( 80 | [[_(BACK), _(CANCEL)]], resize_keyboard=True, one_time_keyboard=True 81 | ) 82 | update.effective_message.reply_text( 83 | _("Send me the other PDF file that you'll like to compare"), 84 | reply_markup=reply_markup, 85 | ) 86 | 87 | return WAIT_SECOND 88 | 89 | 90 | def check_second_doc(update, context): 91 | if not check_user_data(update, context, COMPARE_ID): 92 | return ConversationHandler.END 93 | 94 | result = check_pdf(update, context) 95 | if result == PDF_INVALID_FORMAT: 96 | return WAIT_SECOND 97 | elif result != PDF_OK: 98 | return ConversationHandler.END 99 | 100 | return compare_pdf(update, context) 101 | 102 | 103 | def compare_pdf(update, context): 104 | _ = set_lang(update, context) 105 | message = update.effective_message 106 | message.reply_text( 107 | _("Comparing your PDF files"), reply_markup=ReplyKeyboardRemove() 108 | ) 109 | 110 | with tempfile.NamedTemporaryFile() as tf1, tempfile.NamedTemporaryFile() as tf2: 111 | # Download PDF files 112 | user_data = context.user_data 113 | first_file_id = user_data[COMPARE_ID] 114 | first_file = context.bot.get_file(first_file_id) 115 | first_file.download(custom_path=tf1.name) 116 | second_file = message.document.get_file() 117 | second_file.download(custom_path=tf2.name) 118 | 119 | try: 120 | with tempfile.TemporaryDirectory() as dir_name: 121 | out_fn = os.path.join(dir_name, "Differences.png") 122 | pdf_diff.main(files=[tf1.name, tf2.name], out_file=out_fn) 123 | send_result_file(update, context, out_fn, "compare") 124 | except NoDifferenceError: 125 | message.reply_text( 126 | _("There are no differences in text between your PDF files") 127 | ) 128 | 129 | # Clean up memory and files 130 | if user_data[COMPARE_ID] == first_file_id: 131 | del user_data[COMPARE_ID] 132 | 133 | return ConversationHandler.END 134 | -------------------------------------------------------------------------------- /pdf_bot/commands/merge.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from collections import defaultdict 3 | from threading import Lock 4 | 5 | from PyPDF2 import PdfFileMerger 6 | from PyPDF2.utils import PdfReadError 7 | from telegram import ( 8 | ChatAction, 9 | ParseMode, 10 | ReplyKeyboardMarkup, 11 | ReplyKeyboardRemove, 12 | Update, 13 | ) 14 | from telegram.ext import ( 15 | CallbackContext, 16 | CommandHandler, 17 | ConversationHandler, 18 | Filters, 19 | MessageHandler, 20 | ) 21 | 22 | from pdf_bot.constants import ( 23 | CANCEL, 24 | DONE, 25 | PDF_INVALID_FORMAT, 26 | PDF_TOO_LARGE, 27 | REMOVE_LAST, 28 | TEXT_FILTER, 29 | ) 30 | from pdf_bot.language import set_lang 31 | from pdf_bot.utils import ( 32 | cancel, 33 | check_pdf, 34 | check_user_data, 35 | reply_with_cancel_btn, 36 | send_file_names, 37 | write_send_pdf, 38 | ) 39 | 40 | WAIT_MERGE = 0 41 | MERGE_IDS = "merge_ids" 42 | MERGE_NAMES = "merge_names" 43 | 44 | merge_locks = defaultdict(Lock) 45 | 46 | 47 | def merge_cov_handler() -> ConversationHandler: 48 | handlers = [ 49 | MessageHandler(Filters.document, check_doc, run_async=True), 50 | MessageHandler(TEXT_FILTER, check_text, run_async=True), 51 | ] 52 | conv_handler = ConversationHandler( 53 | entry_points=[CommandHandler("merge", merge, run_async=True)], 54 | states={ 55 | WAIT_MERGE: handlers, 56 | ConversationHandler.WAITING: handlers, 57 | }, 58 | fallbacks=[CommandHandler("cancel", cancel, run_async=True)], 59 | allow_reentry=True, 60 | ) 61 | 62 | return conv_handler 63 | 64 | 65 | def merge(update: Update, context: CallbackContext) -> int: 66 | update.effective_message.chat.send_action(ChatAction.TYPING) 67 | user_id = update.effective_message.from_user.id 68 | merge_locks[user_id].acquire() 69 | context.user_data[MERGE_IDS] = [] 70 | context.user_data[MERGE_NAMES] = [] 71 | merge_locks[user_id].release() 72 | 73 | return ask_first_doc(update, context) 74 | 75 | 76 | def ask_first_doc(update: Update, context: CallbackContext) -> int: 77 | _ = set_lang(update, context) 78 | text = _( 79 | "Send me the PDF files that you'll like to merge\n\n" 80 | "Note that the files will be merged in the order that you send me" 81 | ) 82 | 83 | reply_with_cancel_btn(update, context, text) 84 | 85 | return WAIT_MERGE 86 | 87 | 88 | def check_doc(update: Update, context: CallbackContext) -> int: 89 | message = update.effective_message 90 | message.chat.send_action(ChatAction.TYPING) 91 | result = check_pdf(update, context, send_msg=False) 92 | 93 | if result in [PDF_INVALID_FORMAT, PDF_TOO_LARGE]: 94 | return process_invalid_pdf(update, context, result) 95 | 96 | user_id = message.from_user.id 97 | merge_locks[user_id].acquire() 98 | context.user_data[MERGE_IDS].append(message.document.file_id) 99 | context.user_data[MERGE_NAMES].append(message.document.file_name) 100 | result = ask_next_doc(update, context) 101 | merge_locks[user_id].release() 102 | 103 | return result 104 | 105 | 106 | def process_invalid_pdf( 107 | update: Update, context: CallbackContext, pdf_result: int 108 | ) -> int: 109 | _ = set_lang(update, context) 110 | if pdf_result == PDF_INVALID_FORMAT: 111 | text = _("The file you've sent is not a PDF file") 112 | else: 113 | text = _("The PDF file you've sent is too large for me to download") 114 | 115 | update.effective_message.reply_text(text) 116 | user_id = update.effective_message.from_user.id 117 | merge_locks[user_id].acquire() 118 | 119 | if not context.user_data[MERGE_NAMES]: 120 | result = ask_first_doc(update, context) 121 | else: 122 | result = ask_next_doc(update, context) 123 | 124 | merge_locks[user_id].release() 125 | 126 | return result 127 | 128 | 129 | def ask_next_doc(update: Update, context: CallbackContext) -> int: 130 | _ = set_lang(update, context) 131 | send_file_names(update, context, context.user_data[MERGE_NAMES], _("PDF files")) 132 | reply_markup = ReplyKeyboardMarkup( 133 | [[_(DONE)], [_(REMOVE_LAST), _(CANCEL)]], 134 | resize_keyboard=True, 135 | one_time_keyboard=True, 136 | ) 137 | update.effective_message.reply_text( 138 | _( 139 | "Press Done if you've sent me all the PDF files that " 140 | "you'll like to merge or keep sending me the PDF files" 141 | ), 142 | reply_markup=reply_markup, 143 | parse_mode=ParseMode.HTML, 144 | ) 145 | 146 | return WAIT_MERGE 147 | 148 | 149 | def check_text(update: Update, context: CallbackContext) -> int: 150 | message = update.effective_message 151 | message.chat.send_action(ChatAction.TYPING) 152 | _ = set_lang(update, context) 153 | text = message.text 154 | 155 | if text in [_(REMOVE_LAST), _(DONE)]: 156 | user_id = message.from_user.id 157 | lock = merge_locks[user_id] 158 | 159 | if not check_user_data(update, context, MERGE_IDS, lock): 160 | return ConversationHandler.END 161 | 162 | if text == _(REMOVE_LAST): 163 | return remove_doc(update, context, lock) 164 | elif text == _(DONE): 165 | return preprocess_merge_pdf(update, context, lock) 166 | elif text == _(CANCEL): 167 | return cancel(update, context) 168 | 169 | 170 | def remove_doc(update: Update, context: CallbackContext, lock: Lock) -> int: 171 | _ = set_lang(update, context) 172 | lock.acquire() 173 | file_ids = context.user_data[MERGE_IDS] 174 | file_names = context.user_data[MERGE_NAMES] 175 | file_ids.pop() 176 | file_name = file_names.pop() 177 | 178 | update.effective_message.reply_text( 179 | _("{} has been removed for merging").format(file_name), 180 | parse_mode=ParseMode.HTML, 181 | ) 182 | 183 | if len(file_ids) == 0: 184 | result = ask_first_doc(update, context) 185 | else: 186 | result = ask_next_doc(update, context) 187 | 188 | lock.release() 189 | 190 | return result 191 | 192 | 193 | def preprocess_merge_pdf(update: Update, context: CallbackContext, lock: Lock) -> int: 194 | _ = set_lang(update, context) 195 | lock.acquire() 196 | num_files = len(context.user_data[MERGE_IDS]) 197 | 198 | if num_files == 0: 199 | update.effective_message.reply_text(_("You haven't sent me any PDF files")) 200 | 201 | result = ask_first_doc(update, context) 202 | elif num_files == 1: 203 | update.effective_message.reply_text(_("You've only sent me one PDF file.")) 204 | 205 | result = ask_next_doc(update, context) 206 | else: 207 | result = merge_pdf(update, context) 208 | 209 | lock.release() 210 | 211 | return result 212 | 213 | 214 | def merge_pdf(update: Update, context: CallbackContext) -> int: 215 | _ = set_lang(update, context) 216 | update.effective_message.reply_text( 217 | _("Merging your PDF files"), reply_markup=ReplyKeyboardRemove() 218 | ) 219 | 220 | # Setup temporary files 221 | user_data = context.user_data 222 | file_ids = user_data[MERGE_IDS] 223 | file_names = user_data[MERGE_NAMES] 224 | temp_files = [tempfile.NamedTemporaryFile() for _ in range(len(file_ids))] 225 | merger = PdfFileMerger() 226 | 227 | # Merge PDF files 228 | for i, file_id in enumerate(file_ids): 229 | file_name = temp_files[i].name 230 | file = context.bot.get_file(file_id) 231 | file.download(custom_path=file_name) 232 | 233 | try: 234 | merger.append(open(file_name, "rb")) 235 | except PdfReadError: 236 | update.effective_message.reply_text( 237 | _( 238 | "I can't merge your PDF files as I couldn't open and read \"{}\". " 239 | "Ensure that it is not encrypted" 240 | ).format(file_names[i]) 241 | ) 242 | 243 | return ConversationHandler.END 244 | 245 | # Send result file 246 | write_send_pdf(update, context, merger, "files.pdf", "merged") 247 | 248 | # Clean up memory and files 249 | if user_data[MERGE_IDS] == file_ids: 250 | del user_data[MERGE_IDS] 251 | if user_data[MERGE_NAMES] == file_names: 252 | del user_data[MERGE_NAMES] 253 | for tf in temp_files: 254 | tf.close() 255 | 256 | return ConversationHandler.END 257 | -------------------------------------------------------------------------------- /pdf_bot/commands/photo.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | from collections import defaultdict 4 | from threading import Lock 5 | from typing import List 6 | 7 | import img2pdf 8 | import noteshrink 9 | from telegram import ( 10 | ChatAction, 11 | ParseMode, 12 | ReplyKeyboardMarkup, 13 | ReplyKeyboardRemove, 14 | Update, 15 | ) 16 | from telegram.constants import MAX_FILESIZE_DOWNLOAD 17 | from telegram.ext import ( 18 | CallbackContext, 19 | CommandHandler, 20 | ConversationHandler, 21 | Filters, 22 | MessageHandler, 23 | ) 24 | 25 | from pdf_bot.constants import BEAUTIFY, CANCEL, REMOVE_LAST, TEXT_FILTER, TO_PDF 26 | from pdf_bot.language import set_lang 27 | from pdf_bot.utils import ( 28 | cancel, 29 | check_user_data, 30 | reply_with_cancel_btn, 31 | send_file_names, 32 | send_result_file, 33 | ) 34 | 35 | WAIT_PHOTO = 0 36 | PHOTO_IDS = "photo_ids" 37 | PHOTO_NAMES = "photo_names" 38 | 39 | photo_locks = defaultdict(Lock) 40 | 41 | 42 | def photo_cov_handler() -> ConversationHandler: 43 | handlers = [ 44 | MessageHandler(Filters.document | Filters.photo, check_photo, run_async=True), 45 | MessageHandler(TEXT_FILTER, check_text, run_async=True), 46 | ] 47 | conv_handler = ConversationHandler( 48 | entry_points=[CommandHandler("photo", photo, run_async=True)], 49 | states={ 50 | WAIT_PHOTO: handlers, 51 | ConversationHandler.WAITING: handlers, 52 | }, 53 | fallbacks=[CommandHandler("cancel", cancel, run_async=True)], 54 | allow_reentry=True, 55 | ) 56 | 57 | return conv_handler 58 | 59 | 60 | def photo(update: Update, context: CallbackContext) -> int: 61 | update.effective_message.chat.send_action(ChatAction.TYPING) 62 | user_id = update.effective_message.from_user.id 63 | photo_locks[user_id].acquire() 64 | context.user_data[PHOTO_IDS] = [] 65 | context.user_data[PHOTO_NAMES] = [] 66 | photo_locks[user_id].release() 67 | 68 | return ask_first_photo(update, context) 69 | 70 | 71 | def ask_first_photo(update: Update, context: CallbackContext) -> int: 72 | _ = set_lang(update, context) 73 | text = _( 74 | "Send me the photos that you'll like to beautify or " 75 | "convert into a PDF file\n\nNote that the photos will be beautified and " 76 | "converted in the order that you send me" 77 | ) 78 | reply_with_cancel_btn(update, context, text) 79 | 80 | return WAIT_PHOTO 81 | 82 | 83 | def check_photo(update: Update, context: CallbackContext) -> int: 84 | message = update.effective_message 85 | message.chat.send_action(ChatAction.TYPING) 86 | photo_file = check_photo_file(update, context) 87 | user_id = message.from_user.id 88 | photo_locks[user_id].acquire() 89 | 90 | if photo_file is None: 91 | if not context.user_data[PHOTO_IDS]: 92 | result = ask_first_photo(update, context) 93 | else: 94 | result = ask_next_photo(update, context) 95 | else: 96 | _ = set_lang(update, context) 97 | try: 98 | file_name = photo_file.file_name 99 | except AttributeError: 100 | file_name = _("File name unavailable") 101 | 102 | context.user_data[PHOTO_IDS].append(photo_file.file_id) 103 | context.user_data[PHOTO_NAMES].append(file_name) 104 | result = ask_next_photo(update, context) 105 | 106 | photo_locks[user_id].release() 107 | 108 | return result 109 | 110 | 111 | def check_photo_file(update: Update, context: CallbackContext): 112 | _ = set_lang(update, context) 113 | message = update.effective_message 114 | 115 | if message.document: 116 | photo_file = message.document 117 | if not photo_file.mime_type.startswith("image"): 118 | photo_file = None 119 | message.reply_text(_("The file you've sent is not a photo")) 120 | else: 121 | photo_file = message.photo[-1] 122 | 123 | if photo_file is not None and photo_file.file_size > MAX_FILESIZE_DOWNLOAD: 124 | photo_file = None 125 | message.reply_text(_("The photo you've sent is too large for me to download")) 126 | 127 | return photo_file 128 | 129 | 130 | def ask_next_photo(update: Update, context: CallbackContext) -> int: 131 | _ = set_lang(update, context) 132 | send_file_names(update, context, context.user_data[PHOTO_NAMES], _("photos")) 133 | reply_markup = ReplyKeyboardMarkup( 134 | [[_(BEAUTIFY), _(TO_PDF)], [_(REMOVE_LAST), _(CANCEL)]], 135 | resize_keyboard=True, 136 | one_time_keyboard=True, 137 | ) 138 | update.effective_message.reply_text( 139 | _( 140 | "Select the task from below if you've sent me all the photos, " 141 | "or keep sending me the photos" 142 | ), 143 | reply_markup=reply_markup, 144 | ) 145 | 146 | return WAIT_PHOTO 147 | 148 | 149 | def check_text(update: Update, context: CallbackContext) -> int: 150 | message = update.effective_message 151 | message.chat.send_action(ChatAction.TYPING) 152 | text = update.effective_message.text 153 | result = ConversationHandler.END 154 | _ = set_lang(update, context) 155 | 156 | if text in [_(REMOVE_LAST), _(BEAUTIFY), _(TO_PDF)]: 157 | user_id = message.from_user.id 158 | photo_locks[user_id].acquire() 159 | 160 | if not check_user_data(update, context, PHOTO_IDS): 161 | result = ConversationHandler.END 162 | else: 163 | if text == _(REMOVE_LAST): 164 | result = remove_photo(update, context) 165 | elif text in [_(BEAUTIFY), _(TO_PDF)]: 166 | result = process_all_photos(update, context) 167 | 168 | photo_locks[user_id].release() 169 | elif text == _(CANCEL): 170 | result = cancel(update, context) 171 | 172 | return result 173 | 174 | 175 | def remove_photo(update: Update, context: CallbackContext) -> int: 176 | _ = set_lang(update, context) 177 | file_ids = context.user_data[PHOTO_IDS] 178 | file_names = context.user_data[PHOTO_NAMES] 179 | file_ids.pop() 180 | file_name = file_names.pop() 181 | 182 | update.effective_message.reply_text( 183 | _("{} has been removed for beautifying or converting").format(file_name), 184 | parse_mode=ParseMode.HTML, 185 | ) 186 | 187 | if len(file_ids) == 0: 188 | return ask_first_photo(update, context) 189 | else: 190 | return ask_next_photo(update, context) 191 | 192 | 193 | def process_all_photos(update: Update, context: CallbackContext) -> int: 194 | user_data = context.user_data 195 | file_ids = user_data[PHOTO_IDS] 196 | file_names = user_data[PHOTO_NAMES] 197 | 198 | if update.effective_message.text == BEAUTIFY: 199 | process_photo(update, context, file_ids, is_beautify=True) 200 | else: 201 | process_photo(update, context, file_ids, is_beautify=False) 202 | 203 | # Clean up memory 204 | if user_data[PHOTO_IDS] == file_ids: 205 | del user_data[PHOTO_IDS] 206 | if user_data[PHOTO_NAMES] == file_names: 207 | del user_data[PHOTO_NAMES] 208 | 209 | return ConversationHandler.END 210 | 211 | 212 | def process_photo( 213 | update: Update, context: CallbackContext, file_ids: List[str], is_beautify: bool 214 | ) -> None: 215 | _ = set_lang(update, context) 216 | if is_beautify: 217 | update.effective_message.reply_text( 218 | _("Beautifying and converting your photos"), 219 | reply_markup=ReplyKeyboardRemove(), 220 | ) 221 | else: 222 | update.effective_message.reply_text( 223 | _("Converting your photos into PDF"), reply_markup=ReplyKeyboardRemove() 224 | ) 225 | 226 | # Setup temporary files 227 | temp_files = [tempfile.NamedTemporaryFile() for _ in range(len(file_ids))] 228 | photo_files = [] 229 | 230 | # Download all photos 231 | for i, file_id in enumerate(file_ids): 232 | file_name = temp_files[i].name 233 | photo_file = context.bot.get_file(file_id) 234 | photo_file.download(custom_path=file_name) 235 | photo_files.append(file_name) 236 | 237 | with tempfile.TemporaryDirectory() as dir_name: 238 | if is_beautify: 239 | out_fn = os.path.join(dir_name, "Beautified.pdf") 240 | noteshrink.notescan_main( 241 | photo_files, basename=f"{dir_name}/page", pdfname=out_fn 242 | ) 243 | send_result_file(update, context, out_fn, "beautify") 244 | else: 245 | out_fn = os.path.join(dir_name, "Converted.pdf") 246 | with open(out_fn, "wb") as f: 247 | f.write(img2pdf.convert(photo_files)) 248 | 249 | send_result_file(update, context, out_fn, "to_pdf") 250 | 251 | # Clean up files 252 | for tf in temp_files: 253 | tf.close() 254 | -------------------------------------------------------------------------------- /pdf_bot/commands/text.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | 4 | from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove 5 | from telegram.ext import CommandHandler, ConversationHandler, MessageHandler 6 | from weasyprint import HTML 7 | 8 | from pdf_bot.constants import CANCEL, TEXT_FILTER 9 | from pdf_bot.language import set_lang 10 | from pdf_bot.utils import cancel, send_result_file 11 | 12 | WAIT_TEXT = 0 13 | BASE_HTML = """ 14 | 15 | 16 |

{}

17 | 18 | """ 19 | 20 | 21 | def text_cov_handler(): 22 | conv_handler = ConversationHandler( 23 | entry_points=[CommandHandler("text", ask_text, run_async=True)], 24 | states={WAIT_TEXT: [MessageHandler(TEXT_FILTER, text_to_pdf, run_async=True)]}, 25 | fallbacks=[ 26 | CommandHandler("cancel", cancel, run_async=True), 27 | MessageHandler(TEXT_FILTER, check_text, run_async=True), 28 | ], 29 | allow_reentry=True, 30 | ) 31 | 32 | return conv_handler 33 | 34 | 35 | def ask_text(update, context): 36 | _ = set_lang(update, context) 37 | reply_markup = ReplyKeyboardMarkup( 38 | [[_(CANCEL)]], resize_keyboard=True, one_time_keyboard=True 39 | ) 40 | update.effective_message.reply_text( 41 | _("Send me the text that you'll like to write into your PDF file"), 42 | reply_markup=reply_markup, 43 | ) 44 | 45 | return WAIT_TEXT 46 | 47 | 48 | def check_text(update, context): 49 | _ = set_lang(update, context) 50 | text = update.effective_message.text 51 | 52 | if text == _(CANCEL): 53 | return cancel(update, context) 54 | 55 | 56 | def text_to_pdf(update, context): 57 | _ = set_lang(update, context) 58 | message = update.effective_message 59 | text = message.text 60 | 61 | if text == _(CANCEL): 62 | return cancel(update, context) 63 | 64 | message.reply_text(_("Creating your PDF file"), reply_markup=ReplyKeyboardRemove()) 65 | html = HTML(string=BASE_HTML.format(text.replace("\n", "
"))) 66 | 67 | with tempfile.TemporaryDirectory() as dir_name: 68 | out_fn = os.path.join(dir_name, "Text.pdf") 69 | html.write_pdf(out_fn) 70 | send_result_file(update, context, out_fn, "text") 71 | 72 | return ConversationHandler.END 73 | -------------------------------------------------------------------------------- /pdf_bot/commands/watermark.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | 3 | from PyPDF2 import PdfFileWriter 4 | from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove 5 | from telegram.ext import CommandHandler, ConversationHandler, Filters, MessageHandler 6 | 7 | from pdf_bot.constants import BACK, CANCEL, PDF_INVALID_FORMAT, PDF_OK, TEXT_FILTER 8 | from pdf_bot.language import set_lang 9 | from pdf_bot.utils import cancel, check_pdf, check_user_data, open_pdf, write_send_pdf 10 | 11 | WAIT_SRC = 0 12 | WAIT_WMK = 1 13 | WMK_ID = "watermark_id" 14 | 15 | 16 | def watermark_cov_handler(): 17 | conv_handler = ConversationHandler( 18 | entry_points=[CommandHandler("watermark", watermark, run_async=True)], 19 | states={ 20 | WAIT_SRC: [MessageHandler(Filters.document, check_src_doc, run_async=True)], 21 | WAIT_WMK: [MessageHandler(Filters.document, check_wmk_doc, run_async=True)], 22 | }, 23 | fallbacks=[ 24 | CommandHandler("cancel", cancel, run_async=True), 25 | MessageHandler(TEXT_FILTER, check_text, run_async=True), 26 | ], 27 | allow_reentry=True, 28 | ) 29 | 30 | return conv_handler 31 | 32 | 33 | def watermark(update, context): 34 | return ask_src_doc(update, context) 35 | 36 | 37 | def ask_src_doc(update, context): 38 | _ = set_lang(update, context) 39 | reply_markup = ReplyKeyboardMarkup( 40 | [[_(CANCEL)]], resize_keyboard=True, one_time_keyboard=True 41 | ) 42 | update.effective_message.reply_text( 43 | _("Send me the PDF file that you'll like to add a watermark"), 44 | reply_markup=reply_markup, 45 | ) 46 | 47 | return WAIT_SRC 48 | 49 | 50 | def check_text(update, context): 51 | _ = set_lang(update, context) 52 | text = update.effective_message.text 53 | 54 | if text == _(BACK): 55 | return ask_src_doc(update, context) 56 | elif text == _(CANCEL): 57 | return cancel(update, context) 58 | 59 | 60 | def check_src_doc(update, context): 61 | result = check_pdf(update, context) 62 | if result == PDF_INVALID_FORMAT: 63 | return WAIT_SRC 64 | elif result != PDF_OK: 65 | return ConversationHandler.END 66 | 67 | _ = set_lang(update, context) 68 | context.user_data[WMK_ID] = update.effective_message.document.file_id 69 | 70 | reply_markup = ReplyKeyboardMarkup( 71 | [[_(BACK), _(CANCEL)]], resize_keyboard=True, one_time_keyboard=True 72 | ) 73 | update.effective_message.reply_text( 74 | _("Send me the watermark PDF file"), reply_markup=reply_markup 75 | ) 76 | 77 | return WAIT_WMK 78 | 79 | 80 | def check_wmk_doc(update, context): 81 | if not check_user_data(update, context, WMK_ID): 82 | return ConversationHandler.END 83 | 84 | result = check_pdf(update, context) 85 | if result == PDF_INVALID_FORMAT: 86 | return WAIT_WMK 87 | elif result != PDF_OK: 88 | return ConversationHandler.END 89 | 90 | return add_wmk(update, context) 91 | 92 | 93 | def add_wmk(update, context): 94 | if not check_user_data(update, context, WMK_ID): 95 | return ConversationHandler.END 96 | 97 | _ = set_lang(update, context) 98 | update.effective_message.reply_text( 99 | _("Adding the watermark onto your PDF file"), reply_markup=ReplyKeyboardRemove() 100 | ) 101 | 102 | # Setup temporary files 103 | temp_files = [tempfile.NamedTemporaryFile() for _ in range(2)] 104 | src_fn, wmk_fn = [x.name for x in temp_files] 105 | 106 | user_data = context.user_data 107 | src_file_id = user_data[WMK_ID] 108 | wmk_file_id = update.effective_message.document.file_id 109 | src_reader = open_pdf(update, context, src_file_id, src_fn) 110 | 111 | if src_reader is not None: 112 | wmk_reader = open_pdf(update, context, wmk_file_id, wmk_fn, _("watermark")) 113 | if wmk_reader is not None: 114 | # Add watermark 115 | pdf_writer = PdfFileWriter() 116 | for page in src_reader.pages: 117 | page.mergePage(wmk_reader.getPage(0)) 118 | pdf_writer.addPage(page) 119 | 120 | # Send result file 121 | write_send_pdf(update, context, pdf_writer, "file.pdf", "watermarked") 122 | 123 | # Clean up memory and files 124 | if user_data[WMK_ID] == src_file_id: 125 | del user_data[WMK_ID] 126 | for tf in temp_files: 127 | tf.close() 128 | 129 | return ConversationHandler.END 130 | -------------------------------------------------------------------------------- /pdf_bot/constants.py: -------------------------------------------------------------------------------- 1 | import gettext 2 | 3 | from telegram.ext import Filters 4 | 5 | t = gettext.translation("pdf_bot", localedir="locale", languages=["en_GB"]) 6 | _ = t.gettext 7 | 8 | TEXT_FILTER = Filters.text & ~Filters.command 9 | 10 | # Bot constants 11 | CHANNEL_NAME = "Mr. Developer" 12 | SET_LANG = "set_lang" 13 | 14 | # PDF file validation constants 15 | PDF_OK = 0 16 | PDF_INVALID_FORMAT = 1 17 | PDF_TOO_LARGE = 2 18 | 19 | # PDF file constants 20 | WAIT_DOC_TASK = 0 21 | WAIT_PHOTO_TASK = 1 22 | WAIT_CROP_TYPE = 2 23 | WAIT_CROP_PERCENT = 3 24 | WAIT_CROP_OFFSET = 4 25 | WAIT_DECRYPT_PW = 5 26 | WAIT_ENCRYPT_PW = 6 27 | WAIT_FILE_NAME = 7 28 | WAIT_ROTATE_DEGREE = 8 29 | WAIT_SPLIT_RANGE = 9 30 | WAIT_SCALE_TYPE = 10 31 | WAIT_SCALE_PERCENT = 11 32 | WAIT_SCALE_DIMENSION = 12 33 | WAIT_EXTRACT_PHOTO_TYPE = 13 34 | WAIT_TO_PHOTO_TYPE = 14 35 | WAIT_TEXT_TYPE = 15 36 | 37 | # Keyboard constants 38 | CANCEL = _("Cancel") 39 | DONE = _("Done") 40 | BACK = _("Back") 41 | BY_PERCENT = _("By Percentage") 42 | BY_SIZE = _("By Margin Size") 43 | PREVIEW = _("Preview") 44 | DECRYPT = _("Decrypt") 45 | ENCRYPT = _("Encrypt") 46 | EXTRACT_PHOTO = _("Extract Photos") 47 | TO_PHOTO = _("To Photos") 48 | ROTATE = _("Rotate") 49 | SCALE = _("Scale") 50 | SPLIT = _("Split") 51 | BEAUTIFY = _("Beautify") 52 | TO_PDF = _("To PDF") 53 | RENAME = _("Rename") 54 | CROP = _("Crop") 55 | COMPRESSED = _("Compressed") 56 | PHOTOS = _("Photos") 57 | REMOVE_LAST = _("Remove Last File") 58 | TO_DIMENSIONS = _("To Dimensions") 59 | EXTRACT_TEXT = _("Extract Text") 60 | TEXT_MESSAGE = _("Text Message") 61 | TEXT_FILE = _("Text File") 62 | OCR = "OCR" 63 | COMPRESS = _("Compress") 64 | 65 | # Rotation constants 66 | ROTATE_90 = "90" 67 | ROTATE_180 = "180" 68 | ROTATE_270 = "270" 69 | 70 | # User data constants 71 | PDF_INFO = "pdf_info" 72 | 73 | # Payment Constants 74 | PAYMENT = "payment" 75 | PAYMENT_PAYLOAD = "payment_payload" 76 | CURRENCY = "INR" 77 | PAYMENT_PARA = "payment_para" 78 | THANKS = _("Say Thanks 😁 (₹10)") 79 | COFFEE = _("Coffee ☕ (₹30)") 80 | BOOK = _("Book 📚 (₹50)") 81 | MEAL = _("Meal 🍲 (₹100)") 82 | PAYMENT_DICT = {THANKS: 10, COFFEE: 30, BOOK: 50, MEAL: 100} 83 | WAIT_PAYMENT = 0 84 | 85 | # Datastore constants 86 | USER = "User" 87 | LANGUAGE = "language" 88 | 89 | # Language constants 90 | LANGUAGES = { 91 | "🇬🇧 English (UK)": "en_GB", 92 | "🇺🇸 English (US)": "en_US", 93 | "🇭🇰 廣東話": "zh_HK", 94 | "🇹🇼 繁體中文": "zh_TW", 95 | "🇨🇳 简体中文": "zh_CN", 96 | "🇮🇹 Italiano": "it_IT", 97 | "🇦🇪 ٱلْعَرَبِيَّة‎": "ar_SA", 98 | "🇳🇱 Nederlands": "nl_NL", 99 | "🇧🇷 Português do Brasil": "pt_BR", 100 | "🇪🇸 español": "es_ES", 101 | "🇹🇷 Türkçe": "tr_TR", 102 | "🇮🇱 עברית": "he_IL", 103 | "🇷🇺 русский язык": "ru_RU", 104 | "🇫🇷 français": "fr_FR", 105 | "🇱🇰 සිංහල": "si_LK", 106 | "🇿🇦 Afrikaans": "af_ZA", 107 | "català": "ca_ES", 108 | "🇨🇿 čeština": "cs_CZ", 109 | "🇩🇰 dansk": "da_DK", 110 | "🇫🇮 suomen kieli": "fi_FI", 111 | "🇩🇪 Deutsch": "de_DE", 112 | "🇬🇷 ελληνικά": "el_GR", 113 | "🇭🇺 magyar nyelv": "hu_HU", 114 | "🇯🇵 日本語": "ja_JP", 115 | "🇰🇷 한국어": "ko_KR", 116 | "🇳🇴 norsk": "no_NO", 117 | "🇵🇱 polski": "pl_PL", 118 | "🇵🇹 português": "pt_PT", 119 | "🇷🇴 Daco-Romanian": "ro_RO", 120 | # "🇷🇸 српски језик": "sr_SP", 121 | "🇸🇪 svenska": "sv_SE", 122 | "🇺🇦 українська мова": "uk_UA", 123 | "🇻🇳 Tiếng Việt": "vi_VN", 124 | "🇮🇳 हिन्दी": "hi_IN", 125 | } 126 | 127 | LANGS_SHORT = {x.split("_")[0]: x for x in LANGUAGES.values()} 128 | -------------------------------------------------------------------------------- /pdf_bot/feedback.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | from logbook import Logger 5 | from slack import WebClient 6 | from telegram import ChatAction, Update 7 | from telegram.ext import ( 8 | CallbackContext, 9 | CommandHandler, 10 | ConversationHandler, 11 | MessageHandler, 12 | ) 13 | from textblob import TextBlob 14 | from textblob.exceptions import TranslatorError 15 | 16 | from pdf_bot.constants import CANCEL, TEXT_FILTER 17 | from pdf_bot.language import set_lang 18 | from pdf_bot.utils import cancel, reply_with_cancel_btn 19 | 20 | load_dotenv() 21 | SLACK_TOKEN = os.environ.get("SLACK_TOKEN") 22 | 23 | load_dotenv() 24 | DEV_TELE_ID = int(os.environ.get("DEV_TELE_ID")) 25 | 26 | 27 | def feedback_cov_handler() -> ConversationHandler: 28 | conv_handler = ConversationHandler( 29 | entry_points=[CommandHandler("feedback", feedback, run_async=True)], 30 | states={0: [MessageHandler(TEXT_FILTER, check_text, run_async=True)]}, 31 | fallbacks=[CommandHandler("cancel", cancel, run_async=True)], 32 | ) 33 | 34 | return conv_handler 35 | 36 | 37 | def feedback(update: Update, context: CallbackContext) -> int: 38 | """ 39 | Start the feedback conversation 40 | Args: 41 | update: the update object 42 | context: the context object 43 | 44 | Returns: 45 | The variable indicating to wait for feedback 46 | """ 47 | update.effective_message.chat.send_action(ChatAction.TYPING) 48 | _ = set_lang(update, context) 49 | text = _( 50 | "Send me your feedback (only English feedback will be " 51 | "forwarded to my developer)" 52 | ) 53 | reply_with_cancel_btn(update, context, text) 54 | 55 | return 0 56 | 57 | 58 | def check_text(update: Update, context: CallbackContext) -> int: 59 | update.effective_message.chat.send_action(ChatAction.TYPING) 60 | _ = set_lang(update, context) 61 | if update.effective_message.text == _(CANCEL): 62 | return cancel(update, context) 63 | else: 64 | return receive_feedback(update, context) 65 | 66 | 67 | def receive_feedback(update: Update, context: CallbackContext) -> int: 68 | message = update.effective_message 69 | tele_username = message.chat.username 70 | tele_id = message.chat.id 71 | feedback_msg = message.text 72 | feedback_lang = None 73 | b = TextBlob(feedback_msg) 74 | 75 | try: 76 | feedback_lang = b.detect_language() 77 | except TranslatorError: 78 | pass 79 | 80 | _ = set_lang(update, context) 81 | if feedback_lang is None or feedback_lang.lower() != "en": 82 | message.reply_text(_("The feedback is not in English, try again")) 83 | return 0 84 | 85 | text = "Feedback received from @{} ({}):\n\n{}".format( 86 | tele_username, tele_id, feedback_msg 87 | ) 88 | success = False 89 | 90 | if SLACK_TOKEN is not None: 91 | client = WebClient(token=SLACK_TOKEN) 92 | response = client.chat_postMessage(channel="#pdf-bot-feedback", text=text) 93 | 94 | if response["ok"] and response["message"]["text"] == text: 95 | success = True 96 | 97 | if SLACK_TOKEN is None: 98 | context.bot.send_message(DEV_TELE_ID, text=text) 99 | 100 | if not success: 101 | log = Logger() 102 | log.notice(text) 103 | 104 | message.reply_text( 105 | _("Thank you for your feedback, I've already forwarded it to my developer") 106 | ) 107 | 108 | return ConversationHandler.END 109 | -------------------------------------------------------------------------------- /pdf_bot/files/__init__.py: -------------------------------------------------------------------------------- 1 | from pdf_bot.files.file import file_cov_handler 2 | -------------------------------------------------------------------------------- /pdf_bot/files/compress.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | 4 | import humanize 5 | from telegram import ParseMode, ReplyKeyboardRemove 6 | from telegram.ext import ConversationHandler 7 | 8 | from pdf_bot.constants import PDF_INFO 9 | from pdf_bot.files.utils import run_cmd 10 | from pdf_bot.language import set_lang 11 | from pdf_bot.utils import check_user_data, send_result_file 12 | 13 | 14 | def compress_pdf(update, context): 15 | if not check_user_data(update, context, PDF_INFO): 16 | return ConversationHandler.END 17 | 18 | _ = set_lang(update, context) 19 | update.effective_message.reply_text( 20 | _("Compressing your PDF file"), 21 | reply_markup=ReplyKeyboardRemove(), 22 | ) 23 | 24 | with tempfile.NamedTemporaryFile() as tf: 25 | user_data = context.user_data 26 | file_id, file_name = user_data[PDF_INFO] 27 | pdf_file = context.bot.get_file(file_id) 28 | pdf_file.download(custom_path=tf.name) 29 | 30 | with tempfile.TemporaryDirectory() as dir_name: 31 | out_fn = os.path.join( 32 | dir_name, f"Compressed_{os.path.splitext(file_name)[0]}.pdf" 33 | ) 34 | command = ( 35 | "gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 " 36 | "-dPDFSETTINGS=/default -dNOPAUSE -dQUIET -dBATCH " 37 | f'-sOutputFile="{out_fn}" "{tf.name}"' 38 | ) 39 | 40 | if run_cmd(command): 41 | old_size = os.path.getsize(tf.name) 42 | new_size = os.path.getsize(out_fn) 43 | update.effective_message.reply_text( 44 | _( 45 | "File size reduced by {:.0%}, " 46 | "from {} to {}".format( 47 | (1 - new_size / old_size), 48 | humanize.naturalsize(old_size), 49 | humanize.naturalsize(new_size), 50 | ) 51 | ), 52 | parse_mode=ParseMode.HTML, 53 | ) 54 | send_result_file(update, context, out_fn, "compress") 55 | 56 | else: 57 | update.effective_message.reply_text( 58 | _("Something went wrong, try again") 59 | ) 60 | 61 | # Clean up memory 62 | if user_data[PDF_INFO] == file_id: 63 | del user_data[PDF_INFO] 64 | 65 | return ConversationHandler.END 66 | -------------------------------------------------------------------------------- /pdf_bot/files/crop.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | 4 | from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove 5 | from telegram.ext import ConversationHandler 6 | 7 | from pdf_bot.constants import ( 8 | BACK, 9 | BY_PERCENT, 10 | BY_SIZE, 11 | PDF_INFO, 12 | WAIT_CROP_OFFSET, 13 | WAIT_CROP_PERCENT, 14 | WAIT_CROP_TYPE, 15 | ) 16 | from pdf_bot.files.utils import run_cmd 17 | from pdf_bot.language import set_lang 18 | from pdf_bot.utils import send_result_file 19 | 20 | MIN_PERCENT = 0 21 | MAX_PERCENT = 100 22 | 23 | 24 | def ask_crop_type(update, context): 25 | _ = set_lang(update, context) 26 | keyboard = [[_(BY_PERCENT), _(BY_SIZE)], [_(BACK)]] 27 | reply_markup = ReplyKeyboardMarkup( 28 | keyboard, one_time_keyboard=True, resize_keyboard=True 29 | ) 30 | update.effective_message.reply_text( 31 | _("Select the crop type that you'll like to perform"), reply_markup=reply_markup 32 | ) 33 | 34 | return WAIT_CROP_TYPE 35 | 36 | 37 | def ask_crop_value(update, context): 38 | _ = set_lang(update, context) 39 | message = update.effective_message 40 | reply_markup = ReplyKeyboardMarkup( 41 | [[_(BACK)]], one_time_keyboard=True, resize_keyboard=True 42 | ) 43 | 44 | if message.text == _(BY_PERCENT): 45 | message.reply_text( 46 | _( 47 | "Send me a number between {} and {}. This is the percentage of margin space to " 48 | "retain between the content in your PDF file and the page" 49 | ).format(MIN_PERCENT, MAX_PERCENT), 50 | reply_markup=reply_markup, 51 | ) 52 | 53 | return WAIT_CROP_PERCENT 54 | else: 55 | message.reply_text( 56 | _( 57 | "Send me a number that you'll like to adjust the margin size. " 58 | "Positive numbers will decrease the margin size and negative numbers will increase it" 59 | ), 60 | reply_markup=reply_markup, 61 | ) 62 | 63 | return WAIT_CROP_OFFSET 64 | 65 | 66 | def check_crop_percent(update, context): 67 | _ = set_lang(update, context) 68 | message = update.effective_message 69 | 70 | if message.text == _(BACK): 71 | return ask_crop_type(update, context) 72 | 73 | try: 74 | percent = float(message.text) 75 | except ValueError: 76 | message.reply_text( 77 | _("The number must be between {} and {}, try again").format( 78 | MIN_PERCENT, MAX_PERCENT 79 | ) 80 | ) 81 | 82 | return WAIT_CROP_PERCENT 83 | 84 | return crop_pdf(update, context, percent=percent) 85 | 86 | 87 | def check_crop_size(update, context): 88 | _ = set_lang(update, context) 89 | message = update.effective_message 90 | 91 | if message.text == _(BACK): 92 | return ask_crop_type(update, context) 93 | 94 | try: 95 | offset = float(update.effective_message.text) 96 | except ValueError: 97 | _ = set_lang(update, context) 98 | update.effective_message.reply_text(_("The number is invalid, try again")) 99 | 100 | return WAIT_CROP_OFFSET 101 | 102 | return crop_pdf(update, context, offset=offset) 103 | 104 | 105 | def crop_pdf(update, context, percent=None, offset=None): 106 | _ = set_lang(update, context) 107 | update.effective_message.reply_text( 108 | _("Cropping your PDF file"), reply_markup=ReplyKeyboardRemove() 109 | ) 110 | 111 | with tempfile.NamedTemporaryFile(suffix=".pdf") as tf: 112 | user_data = context.user_data 113 | file_id, file_name = user_data[PDF_INFO] 114 | pdf_file = context.bot.get_file(file_id) 115 | pdf_file.download(custom_path=tf.name) 116 | 117 | with tempfile.TemporaryDirectory() as dir_name: 118 | out_fn = os.path.join(dir_name, f"Cropped_{file_name}") 119 | command = f'pdf-crop-margins -o "{out_fn}" "{tf.name}"' 120 | 121 | if percent is not None: 122 | command += f" -p {percent}" 123 | else: 124 | command += f" -a {offset}" 125 | 126 | if run_cmd(command): 127 | send_result_file(update, context, out_fn, "crop") 128 | else: 129 | update.effective_message.reply_text( 130 | _("Something went wrong, try again") 131 | ) 132 | 133 | # Clean up memory 134 | if user_data[PDF_INFO] == file_id: 135 | del user_data[PDF_INFO] 136 | 137 | return ConversationHandler.END 138 | -------------------------------------------------------------------------------- /pdf_bot/files/crypto.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | 3 | from PyPDF2 import PdfFileReader, PdfFileWriter 4 | from PyPDF2.utils import PdfReadError 5 | from telegram import ReplyKeyboardRemove 6 | from telegram.ext import ConversationHandler 7 | 8 | from pdf_bot.constants import PDF_INFO, WAIT_DECRYPT_PW, WAIT_ENCRYPT_PW 9 | from pdf_bot.files.utils import check_back_user_data, get_back_markup 10 | from pdf_bot.language import set_lang 11 | from pdf_bot.utils import process_pdf, write_send_pdf 12 | 13 | 14 | def ask_decrypt_pw(update, context): 15 | _ = set_lang(update, context) 16 | update.effective_message.reply_text( 17 | _("Send me the password to decrypt your PDF file"), 18 | reply_markup=get_back_markup(update, context), 19 | ) 20 | 21 | return WAIT_DECRYPT_PW 22 | 23 | 24 | def decrypt_pdf(update, context): 25 | result = check_back_user_data(update, context) 26 | if result is not None: 27 | return result 28 | 29 | _ = set_lang(update, context) 30 | message = update.effective_message 31 | message.reply_text( 32 | _("Decrypting your PDF file"), reply_markup=ReplyKeyboardRemove() 33 | ) 34 | 35 | with tempfile.NamedTemporaryFile() as tf: 36 | # Download file 37 | user_data = context.user_data 38 | file_id, file_name = user_data[PDF_INFO] 39 | pdf_file = context.bot.get_file(file_id) 40 | pdf_file.download(custom_path=tf.name) 41 | pdf_reader = None 42 | 43 | try: 44 | pdf_reader = PdfFileReader(open(tf.name, "rb")) 45 | except PdfReadError: 46 | message.reply_text( 47 | _("Your PDF file seems to be invalid and I couldn't open and read it") 48 | ) 49 | 50 | if pdf_reader is not None: 51 | if not pdf_reader.isEncrypted: 52 | message.reply_text(_("Your PDF file is not encrypted")) 53 | else: 54 | try: 55 | if pdf_reader.decrypt(message.text) == 0: 56 | message.reply_text( 57 | _( 58 | "The decryption password is incorrect, try to send it again" 59 | ) 60 | ) 61 | 62 | return WAIT_DECRYPT_PW 63 | 64 | pdf_writer = PdfFileWriter() 65 | for page in pdf_reader.pages: 66 | pdf_writer.addPage(page) 67 | 68 | write_send_pdf(update, context, pdf_writer, file_name, "decrypted") 69 | except NotImplementedError: 70 | message.reply_text( 71 | _( 72 | "Your PDF file is encrypted with a method that I cannot decrypt" 73 | ) 74 | ) 75 | 76 | # Clean up memory 77 | if user_data[PDF_INFO] == file_id: 78 | del user_data[PDF_INFO] 79 | 80 | return ConversationHandler.END 81 | 82 | 83 | def ask_encrypt_pw(update, context): 84 | _ = set_lang(update, context) 85 | update.effective_message.reply_text( 86 | _("Send me the password to encrypt your PDF file"), 87 | reply_markup=get_back_markup(update, context), 88 | ) 89 | 90 | return WAIT_ENCRYPT_PW 91 | 92 | 93 | def encrypt_pdf(update, context): 94 | result = check_back_user_data(update, context) 95 | if result is not None: 96 | return result 97 | 98 | _ = set_lang(update, context) 99 | update.effective_message.reply_text( 100 | _("Encrypting your PDF file"), reply_markup=ReplyKeyboardRemove() 101 | ) 102 | process_pdf(update, context, "encrypted", encrypt_pw=update.effective_message.text) 103 | 104 | return ConversationHandler.END 105 | -------------------------------------------------------------------------------- /pdf_bot/files/document.py: -------------------------------------------------------------------------------- 1 | from telegram import ReplyKeyboardMarkup 2 | 3 | from pdf_bot.constants import ( 4 | CANCEL, 5 | COMPRESS, 6 | CROP, 7 | DECRYPT, 8 | ENCRYPT, 9 | EXTRACT_PHOTO, 10 | EXTRACT_TEXT, 11 | OCR, 12 | PREVIEW, 13 | RENAME, 14 | ROTATE, 15 | SCALE, 16 | SPLIT, 17 | TO_PHOTO, 18 | WAIT_DOC_TASK, 19 | ) 20 | from pdf_bot.utils import set_lang 21 | 22 | 23 | def ask_doc_task(update, context): 24 | _ = set_lang(update, context) 25 | keywords = sorted( 26 | [ 27 | _(DECRYPT), 28 | _(ENCRYPT), 29 | _(ROTATE), 30 | _(SCALE), 31 | _(SPLIT), 32 | _(PREVIEW), 33 | _(TO_PHOTO), 34 | _(EXTRACT_PHOTO), 35 | _(RENAME), 36 | _(CROP), 37 | _(EXTRACT_TEXT), 38 | OCR, 39 | _(COMPRESS), 40 | ] 41 | ) 42 | keywords.append(CANCEL) 43 | keyboard_size = 3 44 | keyboard = [ 45 | keywords[i : i + keyboard_size] for i in range(0, len(keywords), keyboard_size) 46 | ] 47 | 48 | reply_markup = ReplyKeyboardMarkup( 49 | keyboard, resize_keyboard=True, one_time_keyboard=True 50 | ) 51 | update.effective_message.reply_text( 52 | _("Select the task that you'll like to perform"), reply_markup=reply_markup 53 | ) 54 | 55 | return WAIT_DOC_TASK 56 | -------------------------------------------------------------------------------- /pdf_bot/files/file.py: -------------------------------------------------------------------------------- 1 | from telegram.constants import MAX_FILESIZE_DOWNLOAD 2 | from telegram.ext import CommandHandler, ConversationHandler, Filters, MessageHandler 3 | 4 | from pdf_bot.constants import * 5 | from pdf_bot.files.compress import compress_pdf 6 | from pdf_bot.files.crop import ( 7 | ask_crop_type, 8 | ask_crop_value, 9 | check_crop_percent, 10 | check_crop_size, 11 | ) 12 | from pdf_bot.files.crypto import ( 13 | ask_decrypt_pw, 14 | ask_encrypt_pw, 15 | decrypt_pdf, 16 | encrypt_pdf, 17 | ) 18 | from pdf_bot.files.document import ask_doc_task 19 | from pdf_bot.files.ocr import add_ocr_to_pdf 20 | from pdf_bot.files.photo import ( 21 | ask_photo_results_type, 22 | ask_photo_task, 23 | get_pdf_photos, 24 | get_pdf_preview, 25 | pdf_to_photos, 26 | process_photo_task, 27 | ) 28 | from pdf_bot.files.rename import ask_pdf_new_name, rename_pdf 29 | from pdf_bot.files.rotate import ask_rotate_degree, check_rotate_degree 30 | from pdf_bot.files.scale import ( 31 | ask_scale_type, 32 | ask_scale_value, 33 | check_scale_dimension, 34 | check_scale_percent, 35 | ) 36 | from pdf_bot.files.split import ask_split_range, split_pdf 37 | from pdf_bot.files.text import ask_text_type, get_pdf_text 38 | from pdf_bot.language import set_lang 39 | from pdf_bot.utils import cancel 40 | 41 | 42 | def file_cov_handler(): 43 | conv_handler = ConversationHandler( 44 | entry_points=[ 45 | MessageHandler(Filters.document, check_doc, run_async=True), 46 | MessageHandler(Filters.photo, check_photo, run_async=True), 47 | ], 48 | states={ 49 | WAIT_DOC_TASK: [ 50 | MessageHandler(TEXT_FILTER, check_doc_task, run_async=True) 51 | ], 52 | WAIT_PHOTO_TASK: [ 53 | MessageHandler(TEXT_FILTER, check_photo_task, run_async=True) 54 | ], 55 | WAIT_CROP_TYPE: [ 56 | MessageHandler(TEXT_FILTER, check_crop_task, run_async=True) 57 | ], 58 | WAIT_CROP_PERCENT: [ 59 | MessageHandler(TEXT_FILTER, check_crop_percent, run_async=True) 60 | ], 61 | WAIT_CROP_OFFSET: [ 62 | MessageHandler(TEXT_FILTER, check_crop_size, run_async=True) 63 | ], 64 | WAIT_DECRYPT_PW: [MessageHandler(TEXT_FILTER, decrypt_pdf, run_async=True)], 65 | WAIT_ENCRYPT_PW: [MessageHandler(TEXT_FILTER, encrypt_pdf, run_async=True)], 66 | WAIT_FILE_NAME: [MessageHandler(TEXT_FILTER, rename_pdf, run_async=True)], 67 | WAIT_ROTATE_DEGREE: [ 68 | MessageHandler(TEXT_FILTER, check_rotate_degree, run_async=True) 69 | ], 70 | WAIT_SPLIT_RANGE: [MessageHandler(TEXT_FILTER, split_pdf, run_async=True)], 71 | WAIT_TEXT_TYPE: [ 72 | MessageHandler(TEXT_FILTER, check_text_task, run_async=True) 73 | ], 74 | WAIT_SCALE_TYPE: [ 75 | MessageHandler(TEXT_FILTER, check_scale_task, run_async=True) 76 | ], 77 | WAIT_SCALE_PERCENT: [ 78 | MessageHandler(TEXT_FILTER, check_scale_percent, run_async=True) 79 | ], 80 | WAIT_SCALE_DIMENSION: [ 81 | MessageHandler(TEXT_FILTER, check_scale_dimension, run_async=True) 82 | ], 83 | WAIT_EXTRACT_PHOTO_TYPE: [ 84 | MessageHandler(TEXT_FILTER, check_get_photos_task, run_async=True) 85 | ], 86 | WAIT_TO_PHOTO_TYPE: [ 87 | MessageHandler(TEXT_FILTER, check_to_photos_task, run_async=True) 88 | ], 89 | }, 90 | fallbacks=[CommandHandler("cancel", cancel, run_async=True)], 91 | allow_reentry=True, 92 | ) 93 | 94 | return conv_handler 95 | 96 | 97 | def check_doc(update, context): 98 | doc = update.effective_message.document 99 | if doc.mime_type.startswith("image"): 100 | return ask_photo_task(update, context, doc) 101 | elif not doc.mime_type.endswith("pdf"): 102 | return ConversationHandler.END 103 | elif doc.file_size >= MAX_FILESIZE_DOWNLOAD: 104 | _ = set_lang(update, context) 105 | update.effective_message.reply_text( 106 | _( 107 | "Your PDF file is too big for me to download\n\nI can't perform any tasks on it" 108 | ) 109 | ) 110 | 111 | return ConversationHandler.END 112 | 113 | context.user_data[PDF_INFO] = doc.file_id, doc.file_name 114 | 115 | return ask_doc_task(update, context) 116 | 117 | 118 | def check_photo(update, context): 119 | return ask_photo_task(update, context, update.effective_message.photo[-1]) 120 | 121 | 122 | def check_doc_task(update, context): 123 | _ = set_lang(update, context) 124 | text = update.effective_message.text 125 | 126 | if text == _(CROP): 127 | return ask_crop_type(update, context) 128 | elif text == _(DECRYPT): 129 | return ask_decrypt_pw(update, context) 130 | elif text == _(ENCRYPT): 131 | return ask_encrypt_pw(update, context) 132 | elif text in [_(EXTRACT_PHOTO), _(TO_PHOTO)]: 133 | return ask_photo_results_type(update, context) 134 | elif text == _(PREVIEW): 135 | return get_pdf_preview(update, context) 136 | elif text == _(RENAME): 137 | return ask_pdf_new_name(update, context) 138 | elif text == _(ROTATE): 139 | return ask_rotate_degree(update, context) 140 | elif text in [_(SCALE)]: 141 | return ask_scale_type(update, context) 142 | elif text == _(SPLIT): 143 | return ask_split_range(update, context) 144 | elif text == _(EXTRACT_TEXT): 145 | return ask_text_type(update, context) 146 | elif text == OCR: 147 | return add_ocr_to_pdf(update, context) 148 | elif text == COMPRESS: 149 | return compress_pdf(update, context) 150 | elif text == _(CANCEL): 151 | return cancel(update, context) 152 | 153 | 154 | def check_photo_task(update, context): 155 | _ = set_lang(update, context) 156 | text = update.effective_message.text 157 | 158 | if text in [_(BEAUTIFY), _(TO_PDF)]: 159 | return process_photo_task(update, context) 160 | elif text == _(CANCEL): 161 | return cancel(update, context) 162 | 163 | 164 | def check_crop_task(update, context): 165 | _ = set_lang(update, context) 166 | text = update.effective_message.text 167 | 168 | if text in [_(BY_PERCENT), _(BY_SIZE)]: 169 | return ask_crop_value(update, context) 170 | elif text == _(BACK): 171 | return ask_doc_task(update, context) 172 | 173 | 174 | def check_scale_task(update, context): 175 | _ = set_lang(update, context) 176 | text = update.effective_message.text 177 | 178 | if text in [_(BY_PERCENT), _(TO_DIMENSIONS)]: 179 | return ask_scale_value(update, context) 180 | elif text == _(BACK): 181 | return ask_doc_task(update, context) 182 | 183 | 184 | def check_text_task(update, context): 185 | _ = set_lang(update, context) 186 | text = update.effective_message.text 187 | 188 | if text == _(TEXT_MESSAGE): 189 | return get_pdf_text(update, context, is_file=False) 190 | elif text == _(TEXT_FILE): 191 | return get_pdf_text(update, context, is_file=True) 192 | elif text == _(BACK): 193 | return ask_doc_task(update, context) 194 | 195 | 196 | def check_get_photos_task(update, context): 197 | _ = set_lang(update, context) 198 | text = update.effective_message.text 199 | 200 | if text in [_(PHOTOS), _(COMPRESSED)]: 201 | return get_pdf_photos(update, context) 202 | elif text == _(BACK): 203 | return ask_doc_task(update, context) 204 | 205 | 206 | def check_to_photos_task(update, context): 207 | _ = set_lang(update, context) 208 | text = update.effective_message.text 209 | 210 | if text in [_(PHOTOS), _(COMPRESSED)]: 211 | return pdf_to_photos(update, context) 212 | elif text == _(BACK): 213 | return ask_doc_task(update, context) 214 | -------------------------------------------------------------------------------- /pdf_bot/files/ocr.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | 4 | import ocrmypdf 5 | from ocrmypdf.exceptions import PriorOcrFoundError 6 | from telegram import ReplyKeyboardRemove 7 | from telegram.ext import ConversationHandler 8 | 9 | from pdf_bot.constants import PDF_INFO 10 | from pdf_bot.language import set_lang 11 | from pdf_bot.utils import check_user_data, send_result_file 12 | 13 | 14 | def add_ocr_to_pdf(update, context): 15 | if not check_user_data(update, context, PDF_INFO): 16 | return ConversationHandler.END 17 | 18 | _ = set_lang(update, context) 19 | update.effective_message.reply_text( 20 | _("Adding an OCR text layer to your PDF file"), 21 | reply_markup=ReplyKeyboardRemove(), 22 | ) 23 | 24 | with tempfile.NamedTemporaryFile() as tf: 25 | user_data = context.user_data 26 | file_id, file_name = user_data[PDF_INFO] 27 | pdf_file = context.bot.get_file(file_id) 28 | pdf_file.download(custom_path=tf.name) 29 | 30 | with tempfile.TemporaryDirectory() as dir_name: 31 | out_fn = os.path.join(dir_name, f"OCR_{os.path.splitext(file_name)[0]}.pdf") 32 | try: 33 | # logging.getLogger("ocrmypdf").setLevel(logging.WARNING) 34 | ocrmypdf.ocr(tf.name, out_fn, deskew=True, progress_bar=False) 35 | send_result_file(update, context, out_fn, "ocr") 36 | except PriorOcrFoundError: 37 | update.effective_message.reply_text( 38 | _("Your PDF file already has a text layer") 39 | ) 40 | 41 | # Clean up memory 42 | if user_data[PDF_INFO] == file_id: 43 | del user_data[PDF_INFO] 44 | 45 | return ConversationHandler.END 46 | -------------------------------------------------------------------------------- /pdf_bot/files/photo.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | 5 | import pdf2image 6 | from PyPDF2 import PdfFileWriter 7 | from telegram import ChatAction, ReplyKeyboardMarkup, ReplyKeyboardRemove 8 | from telegram.constants import MAX_FILESIZE_DOWNLOAD, MAX_FILESIZE_UPLOAD 9 | from telegram.error import BadRequest 10 | from telegram.ext import ConversationHandler 11 | 12 | from pdf_bot.commands import process_photo 13 | from pdf_bot.constants import ( 14 | BACK, 15 | BEAUTIFY, 16 | CANCEL, 17 | COMPRESSED, 18 | EXTRACT_PHOTO, 19 | PDF_INFO, 20 | PHOTOS, 21 | TO_PDF, 22 | WAIT_EXTRACT_PHOTO_TYPE, 23 | WAIT_PHOTO_TASK, 24 | WAIT_TO_PHOTO_TYPE, 25 | ) 26 | from pdf_bot.files.utils import check_back_user_data, run_cmd 27 | from pdf_bot.language import set_lang 28 | from pdf_bot.stats import update_stats 29 | from pdf_bot.utils import ( 30 | check_user_data, 31 | get_support_markup, 32 | open_pdf, 33 | send_result_file, 34 | ) 35 | 36 | PHOTO_ID = "photo_id" 37 | MAX_MEDIA_GROUP = 10 38 | 39 | 40 | def ask_photo_task(update, context, photo_file): 41 | _ = set_lang(update, context) 42 | message = update.effective_message 43 | 44 | if photo_file.file_size >= MAX_FILESIZE_DOWNLOAD: 45 | message.reply_text( 46 | _( 47 | "Your photo is too large for me to download. " 48 | "I can't beautify or convert your photo" 49 | ) 50 | ) 51 | 52 | return ConversationHandler.END 53 | 54 | context.user_data[PHOTO_ID] = photo_file.file_id 55 | keyboard = [[_(BEAUTIFY), _(TO_PDF)], [_(CANCEL)]] 56 | reply_markup = ReplyKeyboardMarkup( 57 | keyboard, resize_keyboard=True, one_time_keyboard=True 58 | ) 59 | message.reply_text( 60 | _("Select the task that you'll like to perform"), reply_markup=reply_markup 61 | ) 62 | 63 | return WAIT_PHOTO_TASK 64 | 65 | 66 | def process_photo_task(update, context): 67 | """ 68 | Receive the task and perform the task on the photo 69 | Args: 70 | update: the update object 71 | context: the context object 72 | 73 | Returns: 74 | The variable indicating the conversation has ended 75 | """ 76 | if not check_user_data(update, context, PHOTO_ID): 77 | return ConversationHandler.END 78 | 79 | _ = set_lang(update, context) 80 | user_data = context.user_data 81 | file_id = user_data[PHOTO_ID] 82 | 83 | if update.effective_message.text == _(BEAUTIFY): 84 | process_photo(update, context, [file_id], is_beautify=True) 85 | else: 86 | process_photo(update, context, [file_id], is_beautify=False) 87 | 88 | if user_data[PHOTO_ID] == file_id: 89 | del user_data[PHOTO_ID] 90 | 91 | return ConversationHandler.END 92 | 93 | 94 | def get_pdf_preview(update, context): 95 | result = check_back_user_data(update, context) 96 | if result is not None: 97 | return result 98 | 99 | _ = set_lang(update, context) 100 | update.effective_message.reply_text( 101 | _("Extracting a preview for your PDF file"), reply_markup=ReplyKeyboardRemove() 102 | ) 103 | 104 | with tempfile.NamedTemporaryFile() as tf1: 105 | user_data = context.user_data 106 | file_id, file_name = user_data[PDF_INFO] 107 | pdf_reader = open_pdf(update, context, file_id, tf1.name) 108 | 109 | if pdf_reader: 110 | # Get first page of PDF file 111 | pdf_writer = PdfFileWriter() 112 | pdf_writer.addPage(pdf_reader.getPage(0)) 113 | 114 | with tempfile.NamedTemporaryFile() as tf2: 115 | # Write cover preview PDF file 116 | with open(tf2.name, "wb") as f: 117 | pdf_writer.write(f) 118 | 119 | with tempfile.TemporaryDirectory() as dir_name: 120 | # Convert cover preview to JPEG 121 | out_fn = os.path.join( 122 | dir_name, f"Preview_{os.path.splitext(file_name)[0]}.png" 123 | ) 124 | imgs = pdf2image.convert_from_path(tf2.name, fmt="png") 125 | imgs[0].save(out_fn) 126 | 127 | # Send result file 128 | send_result_file(update, context, out_fn, "preview") 129 | 130 | # Clean up memory and files 131 | if user_data[PDF_INFO] == file_id: 132 | del user_data[PDF_INFO] 133 | 134 | return ConversationHandler.END 135 | 136 | 137 | def ask_photo_results_type(update, context): 138 | _ = set_lang(update, context) 139 | if update.effective_message.text == _(EXTRACT_PHOTO): 140 | return_type = WAIT_EXTRACT_PHOTO_TYPE 141 | else: 142 | return_type = WAIT_TO_PHOTO_TYPE 143 | 144 | keyboard = [[_(PHOTOS), _(COMPRESSED)], [_(BACK)]] 145 | reply_markup = ReplyKeyboardMarkup( 146 | keyboard, resize_keyboard=True, one_time_keyboard=True 147 | ) 148 | update.effective_message.reply_text( 149 | _("Select the result file format"), reply_markup=reply_markup 150 | ) 151 | 152 | return return_type 153 | 154 | 155 | def pdf_to_photos(update, context): 156 | if not check_user_data(update, context, PDF_INFO): 157 | return ConversationHandler.END 158 | 159 | _ = set_lang(update, context) 160 | update.effective_message.reply_text( 161 | _("Converting your PDF file into photos"), reply_markup=ReplyKeyboardRemove() 162 | ) 163 | 164 | with tempfile.NamedTemporaryFile() as tf: 165 | user_data = context.user_data 166 | file_id, file_name = user_data[PDF_INFO] 167 | pdf_file = context.bot.get_file(file_id) 168 | pdf_file.download(custom_path=tf.name) 169 | 170 | with tempfile.TemporaryDirectory() as tmp_dir_name: 171 | # Setup the directory for the photos 172 | dir_name = os.path.join(tmp_dir_name, "PDF_Photos") 173 | os.mkdir(dir_name) 174 | 175 | # Convert the PDF file into photos 176 | pdf2image.convert_from_path( 177 | tf.name, 178 | output_folder=dir_name, 179 | output_file=os.path.splitext(file_name)[0], 180 | fmt="png", 181 | ) 182 | 183 | # Handle the result photos 184 | send_result_photos(update, context, dir_name, "to_photos") 185 | 186 | # Clean up memory 187 | if user_data[PDF_INFO] == file_id: 188 | del user_data[PDF_INFO] 189 | 190 | return ConversationHandler.END 191 | 192 | 193 | def get_pdf_photos(update, context): 194 | if not check_user_data(update, context, PDF_INFO): 195 | return ConversationHandler.END 196 | 197 | _ = set_lang(update, context) 198 | update.effective_message.reply_text( 199 | _("Extracting all the photos in your PDF file"), 200 | reply_markup=ReplyKeyboardRemove(), 201 | ) 202 | 203 | with tempfile.NamedTemporaryFile() as tf: 204 | user_data = context.user_data 205 | file_id, file_name = user_data[PDF_INFO] 206 | pdf_file = context.bot.get_file(file_id) 207 | pdf_file.download(custom_path=tf.name) 208 | 209 | with tempfile.TemporaryDirectory() as tmp_dir_name: 210 | dir_name = os.path.join(tmp_dir_name, "Photos_In_PDF") 211 | os.mkdir(dir_name) 212 | if not write_photos_in_pdf(tf.name, dir_name, file_name): 213 | update.effective_message.reply_text( 214 | _("Something went wrong, try again") 215 | ) 216 | else: 217 | if not os.listdir(dir_name): 218 | update.effective_message.reply_text( 219 | _("I couldn't find any photos in your PDF file") 220 | ) 221 | else: 222 | send_result_photos(update, context, dir_name, "get_photos") 223 | 224 | # Clean up memory 225 | if user_data[PDF_INFO] == file_id: 226 | del user_data[PDF_INFO] 227 | 228 | return ConversationHandler.END 229 | 230 | 231 | def write_photos_in_pdf(input_fn, dir_name, file_name): 232 | root_file_name = os.path.splitext(file_name)[0] 233 | image_prefix = os.path.join(dir_name, root_file_name) 234 | command = f'pdfimages -png "{input_fn}" "{image_prefix}"' 235 | 236 | return run_cmd(command) 237 | 238 | 239 | def send_result_photos(update, context, dir_name, task): 240 | _ = set_lang(update, context) 241 | message = update.effective_message 242 | 243 | if message.text == _(PHOTOS): 244 | for photo_name in sorted(os.listdir(dir_name)): 245 | photo_path = os.path.join(dir_name, photo_name) 246 | if os.path.getsize(photo_path) <= MAX_FILESIZE_UPLOAD: 247 | try: 248 | message.chat.send_action(ChatAction.UPLOAD_PHOTO) 249 | message.reply_photo(open(photo_path, "rb")) 250 | except BadRequest: 251 | message.chat.send_action(ChatAction.UPLOAD_DOCUMENT) 252 | message.reply_document(open(photo_path, "rb")) 253 | 254 | message.reply_text( 255 | _("See above for all your photos"), 256 | reply_markup=get_support_markup(update, context), 257 | ) 258 | update_stats(update, task) 259 | else: 260 | # Compress the directory of photos 261 | shutil.make_archive(dir_name, "zip", dir_name) 262 | 263 | # Send result file 264 | send_result_file(update, context, f"{dir_name}.zip", task) 265 | -------------------------------------------------------------------------------- /pdf_bot/files/rename.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | import tempfile 5 | 6 | from telegram import ReplyKeyboardRemove 7 | from telegram.ext import ConversationHandler 8 | from telegram.parsemode import ParseMode 9 | 10 | from pdf_bot.constants import PDF_INFO, WAIT_FILE_NAME 11 | from pdf_bot.files.utils import check_back_user_data, get_back_markup 12 | from pdf_bot.language import set_lang 13 | from pdf_bot.utils import send_result_file 14 | 15 | 16 | def ask_pdf_new_name(update, context): 17 | _ = set_lang(update, context) 18 | update.effective_message.reply_text( 19 | _("Send me the file name that you'll like to rename your PDF file into"), 20 | reply_markup=get_back_markup(update, context), 21 | ) 22 | 23 | return WAIT_FILE_NAME 24 | 25 | 26 | def rename_pdf(update, context): 27 | result = check_back_user_data(update, context) 28 | if result is not None: 29 | return result 30 | 31 | _ = set_lang(update, context) 32 | message = update.effective_message 33 | text = re.sub(r"\.pdf$", "", message.text) 34 | invalid_chars = r"\/*?:\'<>|" 35 | 36 | if set(text) & set(invalid_chars): 37 | message.reply_text( 38 | _( 39 | "File names can't contain any of the following characters:\n{}\n" 40 | "Send me another file name" 41 | ).format(invalid_chars) 42 | ) 43 | 44 | return WAIT_FILE_NAME 45 | 46 | new_fn = "{}.pdf".format(text) 47 | message.reply_text( 48 | _("Renaming your PDF file into {}").format(new_fn), 49 | parse_mode=ParseMode.HTML, 50 | reply_markup=ReplyKeyboardRemove(), 51 | ) 52 | 53 | # Download PDF file 54 | user_data = context.user_data 55 | file_id, _ = user_data[PDF_INFO] 56 | tf = tempfile.NamedTemporaryFile() 57 | pdf_file = context.bot.get_file(file_id) 58 | pdf_file.download(custom_path=tf.name) 59 | 60 | # Rename PDF file 61 | with tempfile.TemporaryDirectory() as dir_name: 62 | out_fn = os.path.join(dir_name, new_fn) 63 | shutil.move(tf.name, out_fn) 64 | send_result_file(update, context, out_fn, "rename") 65 | 66 | # Clean up memory and files 67 | if user_data[PDF_INFO] == file_id: 68 | del user_data[PDF_INFO] 69 | try: 70 | tf.close() 71 | except FileNotFoundError: 72 | pass 73 | 74 | return ConversationHandler.END 75 | -------------------------------------------------------------------------------- /pdf_bot/files/rotate.py: -------------------------------------------------------------------------------- 1 | from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove 2 | from telegram.ext import ConversationHandler 3 | 4 | from pdf_bot.constants import ( 5 | BACK, 6 | PDF_INFO, 7 | ROTATE_90, 8 | ROTATE_180, 9 | ROTATE_270, 10 | WAIT_ROTATE_DEGREE, 11 | ) 12 | from pdf_bot.files.document import ask_doc_task 13 | from pdf_bot.language import set_lang 14 | from pdf_bot.utils import check_user_data, process_pdf 15 | 16 | 17 | def ask_rotate_degree(update, context): 18 | _ = set_lang(update, context) 19 | keyboard = [[ROTATE_90, ROTATE_180], [ROTATE_270, _(BACK)]] 20 | reply_markup = ReplyKeyboardMarkup( 21 | keyboard, resize_keyboard=True, one_time_keyboard=True 22 | ) 23 | update.effective_message.reply_text( 24 | _("Select the degrees that you'll like to rotate your PDF file in clockwise"), 25 | reply_markup=reply_markup, 26 | ) 27 | 28 | return WAIT_ROTATE_DEGREE 29 | 30 | 31 | def check_rotate_degree(update, context): 32 | _ = set_lang(update, context) 33 | text = update.effective_message.text 34 | 35 | if text in [_(ROTATE_90), _(ROTATE_180), _(ROTATE_270)]: 36 | return rotate_pdf(update, context) 37 | elif text == _(BACK): 38 | return ask_doc_task(update, context) 39 | 40 | 41 | def rotate_pdf(update, context): 42 | if not check_user_data(update, context, PDF_INFO): 43 | return ConversationHandler.END 44 | 45 | _ = set_lang(update, context) 46 | degree = int(update.effective_message.text) 47 | update.effective_message.reply_text( 48 | _("Rotating your PDF file clockwise by {} degrees").format(degree), 49 | reply_markup=ReplyKeyboardRemove(), 50 | ) 51 | process_pdf(update, context, "rotated", rotate_degree=degree) 52 | 53 | return ConversationHandler.END 54 | -------------------------------------------------------------------------------- /pdf_bot/files/scale.py: -------------------------------------------------------------------------------- 1 | from telegram import ParseMode, ReplyKeyboardMarkup, ReplyKeyboardRemove 2 | from telegram.ext import ConversationHandler 3 | 4 | from pdf_bot.constants import ( 5 | BACK, 6 | BY_PERCENT, 7 | TO_DIMENSIONS, 8 | WAIT_SCALE_DIMENSION, 9 | WAIT_SCALE_PERCENT, 10 | WAIT_SCALE_TYPE, 11 | ) 12 | from pdf_bot.files.utils import get_back_markup 13 | from pdf_bot.language import set_lang 14 | from pdf_bot.utils import process_pdf 15 | 16 | 17 | def ask_scale_type(update, context): 18 | _ = set_lang(update, context) 19 | keyboard = [[_(BY_PERCENT), _(TO_DIMENSIONS)], [_(BACK)]] 20 | reply_markup = ReplyKeyboardMarkup( 21 | keyboard, one_time_keyboard=True, resize_keyboard=True 22 | ) 23 | update.effective_message.reply_text( 24 | _("Select the scale type that you'll like to perform"), 25 | reply_markup=reply_markup, 26 | ) 27 | 28 | return WAIT_SCALE_TYPE 29 | 30 | 31 | def ask_scale_value(update, context, ask_percent=True): 32 | _ = set_lang(update, context) 33 | message = update.effective_message 34 | reply_markup = get_back_markup(update, context) 35 | 36 | if message.text == _(TO_DIMENSIONS) or not ask_percent: 37 | message.reply_text( 38 | _( 39 | "Send me the width and height\n\nExample: 150 200 " 40 | "(this will set the width to 150 and height to 200)" 41 | ), 42 | reply_markup=reply_markup, 43 | parse_mode=ParseMode.HTML, 44 | ) 45 | 46 | return WAIT_SCALE_DIMENSION 47 | else: 48 | message.reply_text( 49 | _( 50 | "Send me the scaling factors for the horizontal and vertical axes\n\n" 51 | "Example: 2 0.5 (this will double the horizontal axis and " 52 | "halve the vertical axis)" 53 | ), 54 | reply_markup=reply_markup, 55 | parse_mode=ParseMode.HTML, 56 | ) 57 | 58 | return WAIT_SCALE_PERCENT 59 | 60 | 61 | def check_scale_percent(update, context): 62 | _ = set_lang(update, context) 63 | message = update.effective_message 64 | text = message.text 65 | 66 | if text == _(BACK): 67 | return ask_scale_type(update, context) 68 | 69 | try: 70 | x, y = map(float, text.split()) 71 | except ValueError: 72 | message.reply_text( 73 | _("The scaling factors {} are invalid, try again").format(text), 74 | parse_mode=ParseMode.HTML, 75 | ) 76 | return ask_scale_value(update, context) 77 | 78 | return scale_pdf(update, context, percent=(x, y)) 79 | 80 | 81 | def check_scale_dimension(update, context): 82 | _ = set_lang(update, context) 83 | message = update.effective_message 84 | text = message.text 85 | 86 | if text == _(BACK): 87 | return ask_scale_type(update, context) 88 | 89 | try: 90 | x, y = map(float, text.split()) 91 | except ValueError: 92 | message.reply_text( 93 | _("The dimensions {} are invalid, try again").format(text), 94 | parse_mode=ParseMode.HTML, 95 | ) 96 | return ask_scale_value(update, context, ask_percent=False) 97 | 98 | return scale_pdf(update, context, dim=(x, y)) 99 | 100 | 101 | def scale_pdf(update, context, percent=None, dim=None): 102 | _ = set_lang(update, context) 103 | if percent is not None: 104 | update.effective_message.reply_text( 105 | _( 106 | "Scaling your PDF file, horizontally by {} and " 107 | "vertically by {}" 108 | ).format(percent[0], percent[1]), 109 | reply_markup=ReplyKeyboardRemove(), 110 | parse_mode=ParseMode.HTML, 111 | ) 112 | process_pdf(update, context, "scaled", scale_by=percent) 113 | else: 114 | update.effective_message.reply_text( 115 | _( 116 | "Scaling your PDF file with width of {} and height of {}" 117 | ).format(dim[0], dim[1]), 118 | reply_markup=ReplyKeyboardRemove(), 119 | parse_mode=ParseMode.HTML, 120 | ) 121 | process_pdf(update, context, "scaled", scale_to=dim) 122 | 123 | return ConversationHandler.END 124 | -------------------------------------------------------------------------------- /pdf_bot/files/split.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | 3 | from PyPDF2 import PdfFileMerger 4 | from PyPDF2.pagerange import PageRange 5 | from telegram import ReplyKeyboardRemove, Update 6 | from telegram.ext import CallbackContext, ConversationHandler 7 | from telegram.parsemode import ParseMode 8 | 9 | from pdf_bot.constants import PDF_INFO, WAIT_SPLIT_RANGE 10 | from pdf_bot.files.utils import check_back_user_data, get_back_markup 11 | from pdf_bot.language import set_lang 12 | from pdf_bot.utils import open_pdf, write_send_pdf 13 | 14 | 15 | def ask_split_range(update: Update, context: CallbackContext) -> int: 16 | _ = set_lang(update, context) 17 | text = ( 18 | "{intro}\n\n" 19 | "{general}\n" 20 | ": {all}\n" 21 | "7 {eight_only}\n" 22 | "0:3 {first_three}\n" 23 | ":3 {first_three}\n" 24 | "7: {from_eight}\n" 25 | "-1 {last_only}\n" 26 | ":-1 {all_except_last}\n" 27 | "-2 {second_last}\n" 28 | "-2: {last_two}\n" 29 | "-3:-1 {third_second}\n\n" 30 | "{advanced}\n" 31 | "::2 {pages} 0 2 4 ... {to_end}\n" 32 | "1:10:2 {pages} 1 3 5 7 9\n" 33 | "::-1 {all_reversed}\n" 34 | "3:0:-1 {pages} 3 2 1 {except_txt} 0\n" 35 | "2::-1 {pages} 2 1 0" 36 | ).format( 37 | intro=_("Send me the range of pages that you'll like to keep"), 38 | general=_("General usage"), 39 | all=_("all pages"), 40 | eight_only=_("page 8 only"), 41 | first_three=_("first three pages"), 42 | from_eight=_("from page 8 onward"), 43 | last_only=_("last page only"), 44 | all_except_last=_("all pages except the last page"), 45 | second_last=_("second last page only"), 46 | last_two=_("last two pages"), 47 | third_second=_("third and second last pages"), 48 | advanced=_("Advanced usage"), 49 | pages=_("pages"), 50 | to_end=_("to the end"), 51 | all_reversed=_("all pages in reversed order"), 52 | except_txt=_("except"), 53 | ) 54 | update.effective_message.reply_text( 55 | text, 56 | parse_mode=ParseMode.HTML, 57 | reply_markup=get_back_markup(update, context), 58 | ) 59 | 60 | return WAIT_SPLIT_RANGE 61 | 62 | 63 | def split_pdf(update: Update, context: CallbackContext) -> int: 64 | result = check_back_user_data(update, context) 65 | if result is not None: 66 | return result 67 | 68 | _ = set_lang(update, context) 69 | message = update.effective_message 70 | split_range = message.text 71 | 72 | if not PageRange.valid(split_range): 73 | message.reply_text( 74 | _( 75 | "The range is invalid. Try again", 76 | reply_markup=get_back_markup(update, context), 77 | ) 78 | ) 79 | 80 | return WAIT_SPLIT_RANGE 81 | 82 | message.reply_text(_("Splitting your PDF file"), reply_markup=ReplyKeyboardRemove()) 83 | 84 | with tempfile.NamedTemporaryFile() as tf: 85 | user_data = context.user_data 86 | file_id, file_name = user_data[PDF_INFO] 87 | pdf_reader = open_pdf(update, context, file_id, tf.name) 88 | 89 | if pdf_reader is not None: 90 | merger = PdfFileMerger() 91 | merger.append(pdf_reader, pages=PageRange(split_range)) 92 | write_send_pdf(update, context, merger, file_name, "split") 93 | 94 | # Clean up memory 95 | if user_data[PDF_INFO] == file_id: 96 | del user_data[PDF_INFO] 97 | 98 | return ConversationHandler.END 99 | -------------------------------------------------------------------------------- /pdf_bot/files/text.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import textwrap 4 | 5 | from pdfminer.high_level import extract_text_to_fp 6 | from telegram import ParseMode, ReplyKeyboardMarkup, ReplyKeyboardRemove 7 | from telegram.constants import MAX_MESSAGE_LENGTH 8 | from telegram.ext import ConversationHandler 9 | 10 | from pdf_bot.constants import BACK, PDF_INFO, TEXT_FILE, TEXT_MESSAGE, WAIT_TEXT_TYPE 11 | from pdf_bot.language import set_lang 12 | from pdf_bot.utils import check_user_data, send_result_file 13 | 14 | 15 | def ask_text_type(update, context): 16 | _ = set_lang(update, context) 17 | keyboard = [[_(TEXT_MESSAGE), _(TEXT_FILE)], [_(BACK)]] 18 | reply_markup = ReplyKeyboardMarkup( 19 | keyboard, one_time_keyboard=True, resize_keyboard=True 20 | ) 21 | update.effective_message.reply_text( 22 | _("Select how you'll like me to send the text to you"), 23 | reply_markup=reply_markup, 24 | ) 25 | 26 | return WAIT_TEXT_TYPE 27 | 28 | 29 | def get_pdf_text(update, context, is_file): 30 | if not check_user_data(update, context, PDF_INFO): 31 | return ConversationHandler.END 32 | 33 | _ = set_lang(update, context) 34 | update.effective_message.reply_text( 35 | _("Extracting text from your PDF file"), reply_markup=ReplyKeyboardRemove() 36 | ) 37 | 38 | with tempfile.NamedTemporaryFile() as tf: 39 | user_data = context.user_data 40 | file_id, file_name = user_data[PDF_INFO] 41 | pdf_file = context.bot.get_file(file_id) 42 | pdf_file.download(custom_path=tf.name) 43 | 44 | with tempfile.TemporaryDirectory() as dir_name: 45 | tmp_text = tempfile.TemporaryFile() 46 | with open(tf.name, "rb") as f: 47 | extract_text_to_fp(f, tmp_text) 48 | 49 | tmp_text.seek(0) 50 | pdf_texts = textwrap.wrap(tmp_text.read().decode("utf-8").strip()) 51 | out_fn = os.path.join(dir_name, f"{os.path.splitext(file_name)[0]}.txt") 52 | send_pdf_text(update, context, pdf_texts, is_file, out_fn) 53 | 54 | # Clean up memory 55 | if user_data[PDF_INFO] == file_id: 56 | del user_data[PDF_INFO] 57 | 58 | return ConversationHandler.END 59 | 60 | 61 | def send_pdf_text(update, context, pdf_texts, is_file, out_fn): 62 | _ = set_lang(update, context) 63 | message = update.effective_message 64 | 65 | if pdf_texts: 66 | if is_file: 67 | with open(out_fn, "w") as f: 68 | f.write("\n".join(pdf_texts)) 69 | 70 | send_result_file(update, context, out_fn, "get_text") 71 | else: 72 | msg_text = "" 73 | for pdf_text in pdf_texts: 74 | if len(msg_text) + len(pdf_text) + 1 > MAX_MESSAGE_LENGTH: 75 | message.reply_text(msg_text.strip()) 76 | msg_text = "" 77 | 78 | msg_text += f" {pdf_text}" 79 | 80 | if msg_text: 81 | message.reply_text(msg_text.strip()) 82 | 83 | message.reply_text( 84 | _("See above for all the text in your PDF file"), 85 | parse_mode=ParseMode.HTML, 86 | ) 87 | else: 88 | message.reply_text(_("I couldn't find any text in your PDF file")) 89 | -------------------------------------------------------------------------------- /pdf_bot/files/utils.py: -------------------------------------------------------------------------------- 1 | import shlex 2 | from subprocess import PIPE, Popen 3 | 4 | from logbook import Logger 5 | from telegram import ReplyKeyboardMarkup 6 | from telegram.ext import ConversationHandler 7 | 8 | from pdf_bot.constants import BACK, PDF_INFO 9 | from pdf_bot.files.document import ask_doc_task 10 | from pdf_bot.utils import check_user_data, set_lang 11 | 12 | 13 | def get_back_markup(update, context): 14 | _ = set_lang(update, context) 15 | reply_markup = ReplyKeyboardMarkup( 16 | [[_(BACK)]], one_time_keyboard=True, resize_keyboard=True 17 | ) 18 | 19 | return reply_markup 20 | 21 | 22 | def check_back_user_data(update, context): 23 | """ 24 | Check for back action and if user data is valid 25 | Args: 26 | update: the update object 27 | context: the context object 28 | 29 | Returns: 30 | A state if it is a back action of the user data is invalid, else None 31 | """ 32 | _ = set_lang(update, context) 33 | result = None 34 | 35 | if update.effective_message.text == _(BACK): 36 | result = ask_doc_task(update, context) 37 | elif not check_user_data(update, context, PDF_INFO): 38 | result = ConversationHandler.END 39 | 40 | return result 41 | 42 | 43 | def run_cmd(cmd: str) -> bool: 44 | is_success = True 45 | proc = Popen(shlex.split(cmd), stdout=PIPE, stderr=PIPE, shell=False) 46 | out, err = proc.communicate() 47 | 48 | if proc.returncode != 0: 49 | is_success = False 50 | log = Logger() 51 | log.error( 52 | f"Command:\n{cmd}\n\n" 53 | f'Stdout:\n{out.decode("utf-8")}\n\n' 54 | f'Stderr:\n{err.decode("utf-8")}' 55 | ) 56 | 57 | return is_success 58 | -------------------------------------------------------------------------------- /pdf_bot/language.py: -------------------------------------------------------------------------------- 1 | import gettext 2 | 3 | from telegram import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Update 4 | from telegram.chataction import ChatAction 5 | from telegram.ext import CallbackContext 6 | 7 | from pdf_bot.constants import LANGUAGE, LANGUAGES, USER 8 | from pdf_bot.store import client 9 | 10 | 11 | def send_lang(update: Update, context: CallbackContext, query: CallbackQuery = None): 12 | update.effective_message.reply_chat_action(ChatAction.TYPING) 13 | lang = get_lang(update, context, query) 14 | langs = [ 15 | InlineKeyboardButton(key, callback_data=key) 16 | for key, value in sorted(LANGUAGES.items(), key=lambda x: x[1]) 17 | if value != lang 18 | ] 19 | keyboard_size = 2 20 | keyboard = [ 21 | langs[i : i + keyboard_size] for i in range(0, len(langs), keyboard_size) 22 | ] 23 | reply_markup = InlineKeyboardMarkup(keyboard) 24 | 25 | _ = set_lang(update, context) 26 | update.effective_message.reply_text( 27 | _("Select your language"), reply_markup=reply_markup 28 | ) 29 | 30 | 31 | def get_lang(update, context, query=None): 32 | if context.user_data is not None and LANGUAGE in context.user_data: 33 | lang = context.user_data[LANGUAGE] 34 | else: 35 | if query is None: 36 | user_id = update.effective_message.from_user.id 37 | else: 38 | user_id = query.from_user.id 39 | 40 | user_key = client.key(USER, user_id) 41 | user = client.get(key=user_key) 42 | 43 | if user is None or LANGUAGE not in user: 44 | lang = "en_GB" 45 | else: 46 | lang = user[LANGUAGE] 47 | if lang == "en": 48 | lang = "en_GB" 49 | 50 | context.user_data[LANGUAGE] = lang 51 | 52 | return lang 53 | 54 | 55 | def store_lang(update, context, query): 56 | lang_code = LANGUAGES[query.data] 57 | with client.transaction(): 58 | user_key = client.key(USER, query.from_user.id) 59 | user = client.get(key=user_key) 60 | user[LANGUAGE] = lang_code 61 | client.put(user) 62 | 63 | context.user_data[LANGUAGE] = lang_code 64 | _ = set_lang(update, context) 65 | query.message.edit_text(_("Your language has been set to {}").format(query.data)) 66 | 67 | 68 | def set_lang(update, context, query=None): 69 | lang = get_lang(update, context, query) 70 | t = gettext.translation("pdf_bot", localedir="locale", languages=[lang]) 71 | 72 | return t.gettext 73 | -------------------------------------------------------------------------------- /pdf_bot/mq_bot.py: -------------------------------------------------------------------------------- 1 | import telegram.bot 2 | from telegram.ext import messagequeue as mq 3 | 4 | 5 | class MQBot(telegram.bot.Bot): 6 | """A subclass of Bot which delegates send method handling to MQ""" 7 | 8 | def __init__(self, *args, is_queued_def=True, mqueue=None, **kwargs): 9 | super(MQBot, self).__init__(*args, **kwargs) 10 | # below 2 attributes should be provided for decorator usage 11 | self._is_messages_queued_default = is_queued_def 12 | self._msg_queue = mqueue or mq.MessageQueue() 13 | 14 | def __del__(self): 15 | try: 16 | self._msg_queue.stop() 17 | except: 18 | pass 19 | super(MQBot, self).__del__() 20 | 21 | @mq.queuedmessage 22 | def send_message(self, *args, **kwargs): 23 | """Wrapped method would accept new `queued` and `isgroup` 24 | OPTIONAL arguments""" 25 | return super(MQBot, self).send_message(*args, **kwargs) 26 | -------------------------------------------------------------------------------- /pdf_bot/payment.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from dotenv import load_dotenv 5 | from telegram import ( 6 | ForceReply, 7 | InlineKeyboardButton, 8 | InlineKeyboardMarkup, 9 | LabeledPrice, 10 | ReplyKeyboardRemove, 11 | ) 12 | 13 | from pdf_bot.constants import * 14 | from pdf_bot.language import set_lang 15 | 16 | load_dotenv() 17 | STRIPE_TOKEN = os.environ.get("STRIPE_TOKEN", os.environ.get("STRIPE_TOKEN_BETA")) 18 | 19 | 20 | def receive_custom_amount(update, context): 21 | _ = set_lang(update, context) 22 | if _(CUSTOM_MSG) in update.effective_message.reply_to_message.text: 23 | try: 24 | amount = round(float(update.effective_message.text)) 25 | if amount <= 0: 26 | raise ValueError 27 | 28 | send_payment_invoice(update, context, amount=amount) 29 | except ValueError: 30 | _ = set_lang(update, context) 31 | update.effective_message.reply_text( 32 | _("The amount you sent is invalid, try again. {}").format( 33 | _(CUSTOM_MSG) 34 | ), 35 | reply_markup=ForceReply(), 36 | ) 37 | 38 | 39 | def send_support_options(update, context, query=None): 40 | _ = set_lang(update, context, query) 41 | keyboard = [ 42 | [ 43 | InlineKeyboardButton(_(THANKS), callback_data=THANKS), 44 | InlineKeyboardButton(_(COFFEE), callback_data=COFFEE), 45 | ], 46 | [ 47 | InlineKeyboardButton(_(BEER), callback_data=BEER), 48 | InlineKeyboardButton(_(MEAL), callback_data=MEAL), 49 | ], 50 | [InlineKeyboardButton(_(CUSTOM), callback_data=CUSTOM)], 51 | [ 52 | InlineKeyboardButton( 53 | _("Help translate PDF Bot"), "https://crwd.in/telegram-pdf-bot" 54 | ) 55 | ], 56 | ] 57 | reply_markup = InlineKeyboardMarkup(keyboard) 58 | text = _("Select how you want to support PDF Bot") 59 | 60 | if query is None: 61 | user_id = update.effective_message.from_user.id 62 | else: 63 | user_id = query.from_user.id 64 | 65 | context.bot.send_message(user_id, text, reply_markup=reply_markup) 66 | 67 | 68 | def send_payment_invoice(update, context, query=None, amount=None): 69 | if query is None: 70 | message = update.effective_message 71 | label = message.text 72 | else: 73 | message = query.message 74 | label = query.data 75 | 76 | _ = set_lang(update, context) 77 | chat_id = message.chat_id 78 | title = _("Support PDF Bot") 79 | description = _("Say thanks to PDF Bot and help keep it running") 80 | 81 | if amount is None: 82 | price = PAYMENT_DICT[label] 83 | else: 84 | label = CUSTOM 85 | price = amount 86 | 87 | prices = [LabeledPrice(re.sub(r"\s\(.*", "", label), price * 100)] 88 | 89 | context.bot.send_invoice( 90 | chat_id, 91 | title, 92 | description, 93 | PAYMENT_PAYLOAD, 94 | STRIPE_TOKEN, 95 | PAYMENT_PARA, 96 | CURRENCY, 97 | prices, 98 | ) 99 | 100 | 101 | def precheckout_check(update, context): 102 | _ = set_lang(update, context) 103 | query = update.pre_checkout_query 104 | 105 | if query.invoice_payload != PAYMENT_PAYLOAD: 106 | query.answer(ok=False, error_message=_("Something went wrong")) 107 | else: 108 | query.answer(ok=True) 109 | 110 | 111 | def successful_payment(update, context): 112 | _ = set_lang(update, context) 113 | update.effective_message.reply_text( 114 | _("Thank you for your support!"), reply_markup=ReplyKeyboardRemove() 115 | ) 116 | -------------------------------------------------------------------------------- /pdf_bot/stats.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | 3 | matplotlib.use("Agg") 4 | 5 | import os 6 | import tempfile 7 | from collections import defaultdict 8 | from datetime import date 9 | 10 | import matplotlib.pyplot as plt 11 | from dotenv import load_dotenv 12 | from google.cloud import datastore 13 | 14 | from pdf_bot.constants import LANGUAGE, LANGUAGES, USER 15 | from pdf_bot.store import client 16 | 17 | load_dotenv() 18 | DEV_TELE_ID = int(os.environ.get("DEV_TELE_ID")) 19 | 20 | 21 | def update_stats(update, task): 22 | user_key = client.key(USER, update.effective_message.from_user.id) 23 | with client.transaction(): 24 | user = client.get(key=user_key) 25 | if user is None: 26 | user = datastore.Entity(user_key) 27 | user[task] = 1 28 | else: 29 | if task in user: 30 | user[task] += 1 31 | else: 32 | user[task] = 1 33 | 34 | client.put(user) 35 | 36 | 37 | def get_stats(update, context): 38 | query = client.query(kind=USER) 39 | num_users = num_tasks = 0 40 | counts = defaultdict(int) 41 | langs = defaultdict(int) 42 | 43 | for user in query.fetch(): 44 | if user.key.id != DEV_TELE_ID: 45 | num_users += 1 46 | for key in user.keys(): 47 | if key != LANGUAGE: 48 | num_tasks += user[key] 49 | if key != "count": 50 | counts[key] += user[key] 51 | elif key == LANGUAGE: 52 | lang = user[key] 53 | if lang == "en": 54 | lang = "en_GB" 55 | 56 | langs[lang] += 1 57 | 58 | launch_date = date(2021, 2, 1) 59 | stats_date = date(2021, 2, 1) 60 | curr_date = date.today() 61 | 62 | launch_diff = (curr_date - launch_date).days 63 | stats_diff = (curr_date - stats_date).days 64 | est_num_tasks = int(num_tasks / stats_diff * launch_diff * 0.8) 65 | 66 | update.effective_message.reply_text( 67 | f"Total users: {num_users:,}\nTotal tasks: {num_tasks:,}\n" 68 | f"Estimated total tasks: {est_num_tasks:,}" 69 | ) 70 | 71 | text = "Language stats:\n" 72 | for key, value in LANGUAGES.items(): 73 | if value in langs: 74 | text += f"{key}: {langs[value]:,}\n" 75 | 76 | update.effective_message.reply_text(text) 77 | send_plot(update, counts) 78 | 79 | 80 | def send_plot(update, counts): 81 | tasks = sorted(counts.keys()) 82 | nums = [counts[x] for x in tasks] 83 | y_pos = list(range(len(tasks))) 84 | 85 | plt.rcdefaults() 86 | _, ax = plt.subplots() 87 | 88 | ax.barh(y_pos, nums, align="center") 89 | ax.set_yticks(y_pos) 90 | ax.set_yticklabels(tasks) 91 | ax.set_xlabel("Counts") 92 | ax.set_ylabel("Tasks") 93 | ax.invert_yaxis() 94 | ax.set_title("PDF Bot Statistics") 95 | ax.get_xaxis().set_major_formatter( 96 | matplotlib.ticker.FuncFormatter(lambda x, _: f"{int(x):,}") 97 | ) 98 | plt.tight_layout() 99 | 100 | with tempfile.NamedTemporaryFile(suffix=".png") as tf: 101 | plt.savefig(tf.name) 102 | update.effective_message.reply_photo(open(tf.name, "rb")) 103 | -------------------------------------------------------------------------------- /pdf_bot/store.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | from google.cloud import datastore 5 | from telegram import User 6 | 7 | from pdf_bot.constants import LANGS_SHORT, LANGUAGE, USER 8 | 9 | load_dotenv() 10 | GCP_KEY_FILE = os.environ.get("GCP_KEY_FILE") 11 | GCP_CRED = os.environ.get("GCP_CRED") 12 | 13 | if GCP_CRED is None: 14 | with open(GCP_KEY_FILE, "w") as f: 15 | f.write(GCP_CRED) 16 | 17 | if GCP_KEY_FILE is not None: 18 | client = datastore.Client.from_service_account_json(GCP_KEY_FILE) 19 | else: 20 | client = datastore.Client() 21 | 22 | 23 | def create_user(tele_user: User) -> None: 24 | key = client.key(USER, tele_user.id) 25 | user_lang_code = tele_user.language_code 26 | lang_code = "en_GB" 27 | 28 | if ( 29 | user_lang_code is not None 30 | and user_lang_code != "en" 31 | and user_lang_code in LANGS_SHORT 32 | ): 33 | lang_code = LANGS_SHORT[user_lang_code] 34 | 35 | with client.transaction(): 36 | db_user = client.get(key=key) 37 | if db_user is None: 38 | db_user = datastore.Entity(key) 39 | if LANGUAGE not in db_user: 40 | db_user[LANGUAGE] = lang_code 41 | 42 | client.put(db_user) 43 | -------------------------------------------------------------------------------- /pdf_bot/url.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import tempfile 4 | from urllib.parse import urlparse 5 | 6 | from telegram import Update 7 | from telegram.ext import CallbackContext 8 | from weasyprint import HTML 9 | from weasyprint.urls import URLFetchingError 10 | 11 | from pdf_bot.language import set_lang 12 | from pdf_bot.utils import send_result_file 13 | 14 | URLS = "urls" 15 | logging.getLogger("weasyprint").setLevel(100) 16 | 17 | 18 | def url_to_pdf(update: Update, context: CallbackContext): 19 | _ = set_lang(update, context) 20 | message = update.effective_message 21 | url = message.text 22 | user_data = context.user_data 23 | 24 | if user_data is not None and URLS in user_data and url in user_data[URLS]: 25 | message.reply_text( 26 | _("You've sent me this web page already and I'm still converting it") 27 | ) 28 | else: 29 | message.reply_text(_("Converting your web page into a PDF file")) 30 | if URLS in user_data: 31 | user_data[URLS].add(url) 32 | else: 33 | user_data[URLS] = {url} 34 | 35 | with tempfile.TemporaryDirectory() as dir_name: 36 | out_fn = os.path.join(dir_name, f"{urlparse(url).netloc}.pdf") 37 | try: 38 | HTML(url=url).write_pdf(out_fn) 39 | send_result_file(update, context, out_fn, "url") 40 | except URLFetchingError: 41 | message.reply_text(_("Unable to reach your web page")) 42 | 43 | user_data[URLS].remove(url) 44 | -------------------------------------------------------------------------------- /pdf_bot/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | from threading import Lock 4 | 5 | from PyPDF2 import PdfFileReader, PdfFileWriter 6 | from PyPDF2.utils import PdfReadError 7 | from telegram import ( 8 | ChatAction, 9 | InlineKeyboardButton, 10 | InlineKeyboardMarkup, 11 | ReplyKeyboardMarkup, 12 | ReplyKeyboardRemove, 13 | Update, 14 | ) 15 | from telegram.constants import MAX_FILESIZE_DOWNLOAD, MAX_FILESIZE_UPLOAD 16 | from telegram.ext import CallbackContext, ConversationHandler 17 | 18 | from pdf_bot.constants import ( 19 | CANCEL, 20 | CHANNEL_NAME, 21 | PAYMENT, 22 | PDF_INFO, 23 | PDF_INVALID_FORMAT, 24 | PDF_OK, 25 | PDF_TOO_LARGE, 26 | ) 27 | from pdf_bot.language import set_lang 28 | from pdf_bot.stats import update_stats 29 | 30 | 31 | def cancel(update, context): 32 | _ = set_lang(update, context) 33 | update.effective_message.reply_text( 34 | _("Action cancelled"), reply_markup=ReplyKeyboardRemove() 35 | ) 36 | 37 | return ConversationHandler.END 38 | 39 | 40 | def reply_with_cancel_btn(update: Update, context: CallbackContext, text: str): 41 | _ = set_lang(update, context) 42 | reply_markup = ReplyKeyboardMarkup( 43 | [[_(CANCEL)]], resize_keyboard=True, one_time_keyboard=True 44 | ) 45 | update.effective_message.reply_text(text, reply_markup=reply_markup) 46 | 47 | 48 | def check_pdf(update, context, send_msg=True): 49 | """ 50 | Validate the PDF file 51 | Args: 52 | update: the update object 53 | context: the context object 54 | send_msg: the bool indicating to send a message or not 55 | 56 | Returns: 57 | The variable indicating the validation result 58 | """ 59 | pdf_status = PDF_OK 60 | message = update.effective_message 61 | pdf_file = message.document 62 | _ = set_lang(update, context) 63 | 64 | if not pdf_file.mime_type.endswith("pdf"): 65 | pdf_status = PDF_INVALID_FORMAT 66 | if send_msg: 67 | message.reply_text(_("The file you sent is not a PDF file, try again")) 68 | elif pdf_file.file_size >= MAX_FILESIZE_DOWNLOAD: 69 | pdf_status = PDF_TOO_LARGE 70 | if send_msg: 71 | message.reply_text( 72 | _( 73 | "The PDF file you sent is too large for me to download\n\n" 74 | "I've cancelled your action" 75 | ) 76 | ) 77 | 78 | return pdf_status 79 | 80 | 81 | def check_user_data( 82 | update: Update, context: CallbackContext, key: str, lock: Lock = None 83 | ) -> bool: 84 | """ 85 | Check if the specified key exists in user_data 86 | Args: 87 | update: the update object 88 | context: the context object 89 | key: the string of key 90 | 91 | Returns: 92 | The boolean indicating if the key exists or not 93 | """ 94 | data_ok = True 95 | if lock is not None: 96 | lock.acquire() 97 | 98 | if key not in context.user_data: 99 | data_ok = False 100 | _ = set_lang(update, context) 101 | update.effective_message.reply_text(_("Something went wrong, start over again")) 102 | 103 | if lock is not None: 104 | lock.release() 105 | 106 | return data_ok 107 | 108 | 109 | def process_pdf( 110 | update, 111 | context, 112 | file_type, 113 | encrypt_pw=None, 114 | rotate_degree=None, 115 | scale_by=None, 116 | scale_to=None, 117 | ): 118 | """ 119 | Process different PDF file manipulations 120 | Args: 121 | update: the update object 122 | context: the context object 123 | file_type: the string of file type 124 | encrypt_pw: the string of encryption password 125 | rotate_degree: the int of rotation degree 126 | scale_by: the tuple of scale by values 127 | scale_to: the tuple of scale to values 128 | 129 | Returns: 130 | None 131 | """ 132 | with tempfile.NamedTemporaryFile() as tf: 133 | user_data = context.user_data 134 | file_id, file_name = user_data[PDF_INFO] 135 | 136 | if encrypt_pw is not None: 137 | pdf_reader = open_pdf(update, context, file_id, tf.name, file_type) 138 | else: 139 | pdf_reader = open_pdf(update, context, file_id, tf.name) 140 | 141 | if pdf_reader is not None: 142 | pdf_writer = PdfFileWriter() 143 | for page in pdf_reader.pages: 144 | if rotate_degree is not None: 145 | pdf_writer.addPage(page.rotateClockwise(rotate_degree)) 146 | elif scale_by is not None: 147 | page.scale(scale_by[0], scale_by[1]) 148 | pdf_writer.addPage(page) 149 | elif scale_to is not None: 150 | page.scaleTo(scale_to[0], scale_to[1]) 151 | pdf_writer.addPage(page) 152 | else: 153 | pdf_writer.addPage(page) 154 | 155 | if encrypt_pw is not None: 156 | pdf_writer.encrypt(encrypt_pw) 157 | 158 | # Send result file 159 | write_send_pdf(update, context, pdf_writer, file_name, file_type) 160 | 161 | # Clean up memory 162 | if user_data[PDF_INFO] == file_id: 163 | del user_data[PDF_INFO] 164 | 165 | 166 | def open_pdf(update, context, file_id, file_name, file_type=None): 167 | """ 168 | Download, open and validate PDF file 169 | Args: 170 | update: the update object 171 | context: the context object 172 | file_id: the string of the file ID 173 | file_name: the string of the file name 174 | file_type: the string of the file type 175 | 176 | Returns: 177 | The PdfFileReader object or None 178 | """ 179 | _ = set_lang(update, context) 180 | pdf_file = context.bot.get_file(file_id) 181 | pdf_file.download(custom_path=file_name) 182 | pdf_reader = None 183 | 184 | try: 185 | pdf_reader = PdfFileReader(open(file_name, "rb")) 186 | except PdfReadError: 187 | update.effective_message.reply_text( 188 | _( 189 | "Your PDF file seems to be invalid and I couldn't open and read it\n\n" 190 | "I've cancelled your action" 191 | ) 192 | ) 193 | 194 | if pdf_reader is not None and pdf_reader.isEncrypted: 195 | if file_type is not None: 196 | if file_type == "encrypted": 197 | text = _("Your PDF file is already encrypted") 198 | else: 199 | text = _( 200 | "Your {} PDF file is encrypted and you'll have to decrypt it first\n\n" 201 | "I've cancelled your action" 202 | ).format(file_type) 203 | else: 204 | text = _( 205 | "Your PDF file is encrypted and you'll have to decrypt it first\n\n" 206 | "I've cancelled your action" 207 | ) 208 | 209 | pdf_reader = None 210 | update.effective_message.reply_text(text) 211 | 212 | return pdf_reader 213 | 214 | 215 | def send_file_names(update, context, file_names, file_type): 216 | """ 217 | Send a list of file names to user 218 | Args: 219 | update: the update object 220 | context: the context object 221 | file_names: the list of file names 222 | file_type: the string of file type 223 | 224 | Returns: 225 | None 226 | """ 227 | _ = set_lang(update, context) 228 | text = _("You've sent me these {} so far:\n").format(file_type) 229 | for i, filename in enumerate(file_names): 230 | text += f"{i + 1}: {filename}\n" 231 | 232 | update.effective_message.reply_text(text) 233 | 234 | 235 | def write_send_pdf(update, context, pdf_writer, file_name, task): 236 | """ 237 | Write and send result PDF file to user 238 | Args: 239 | update: the update object 240 | context: the context object 241 | pdf_writer: the PdfFileWriter object 242 | file_name: the string of the file name 243 | task: the string of the task 244 | 245 | Returns: 246 | None 247 | """ 248 | with tempfile.TemporaryDirectory() as dir_name: 249 | new_fn = f"{task.title()}_{file_name}" 250 | out_fn = os.path.join(dir_name, new_fn) 251 | 252 | with open(out_fn, "wb") as f: 253 | pdf_writer.write(f) 254 | 255 | send_result_file(update, context, out_fn, task) 256 | 257 | 258 | def send_result_file(update, context, out_fn, task): 259 | """ 260 | Send result file to user 261 | Args: 262 | update: the update object 263 | context: the context object 264 | out_fn: the string of the output file name 265 | task: the string of the task 266 | 267 | Returns: 268 | None 269 | """ 270 | _ = set_lang(update, context) 271 | message = update.effective_message 272 | reply_markup = get_support_markup(update, context) 273 | 274 | if os.path.getsize(out_fn) >= MAX_FILESIZE_UPLOAD: 275 | message.reply_text( 276 | _("The result file is too large for me to send to you"), 277 | reply_markup=reply_markup, 278 | ) 279 | else: 280 | if out_fn.endswith(".png"): 281 | message.chat.send_action(ChatAction.UPLOAD_PHOTO) 282 | message.reply_photo( 283 | open(out_fn, "rb"), 284 | caption=_("Here is your result file"), 285 | reply_markup=reply_markup, 286 | ) 287 | else: 288 | message.chat.send_action(ChatAction.UPLOAD_DOCUMENT) 289 | message.reply_document( 290 | document=open(out_fn, "rb"), 291 | caption=_("Here is your result file"), 292 | reply_markup=reply_markup, 293 | ) 294 | 295 | update_stats(update, task) 296 | 297 | 298 | def get_support_markup(update, context): 299 | """ 300 | Create the reply markup 301 | Returns: 302 | The reply markup object 303 | """ 304 | _ = set_lang(update, context) 305 | keyboard = [ 306 | [ 307 | InlineKeyboardButton(_("Join Channel"), f"https://t.me/{CHANNEL_NAME}"), 308 | InlineKeyboardButton(_("Support PDF Bot"), callback_data=PAYMENT), 309 | ] 310 | ] 311 | reply_markup = InlineKeyboardMarkup(keyboard) 312 | 313 | return reply_markup 314 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.isort] 2 | profile = "black" 3 | multi_line_output = 3 -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "labels": ["dependencies"], 4 | "stabilityDays": 3, 5 | "prCreation": "not-pending", 6 | "schedule": ["after 6pm every day", "every weekend"], 7 | "timezone": "Australia/Sydney", 8 | "packageRules": [ 9 | { 10 | "matchPaths": [".github/workflows/*.yml"], 11 | "labels": ["github-actions"], 12 | "automerge": true 13 | }, 14 | { 15 | "matchFiles": ["Dockerfile"], 16 | "labels": ["docker"], 17 | "automerge": true 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Babel 2 | google-cloud-datastore==2.10.0 3 | humanize 4 | img2pdf 5 | Logbook 6 | matplotlib 7 | ocrmypdf 8 | pdfCropMargins 9 | pdf2image 10 | pdfminer.six 11 | python-dotenv 12 | python-telegram-bot==13.12 13 | Pillow 14 | PyPDF2 15 | requests 16 | slackclient 17 | textblob 18 | WeasyPrint 19 | git+https://github.com/zeshuaro/noteshrink.git#egg=noteshrink 20 | git+https://github.com/zeshuaro/pdf-diff#egg=pdf_diff 21 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.1 2 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | python3 bot.py 2 | --------------------------------------------------------------------------------