├── .deepsource.toml ├── .dockerignore ├── .env ├── .github ├── CODE_OF_CONDUCT.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.yml │ └── FEATURE_REQUEST.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── authors.yml │ ├── black-code-style.yml │ ├── build-docker-image.yml │ ├── codeql-analysis.yml │ ├── contributors.yml │ ├── potential-duplicates.yml │ ├── pr-triage-dummy.yml │ ├── pr-triage.yml │ ├── publish-docker-image.yml │ └── unfurl-links.yml ├── .gitignore ├── .python-version ├── .vscode ├── extensions.json └── settings.json ├── .whitesource ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTORS.svg ├── CreateMongoDB.md ├── Dockerfile ├── LICENSE ├── README.md ├── app.json ├── bot_thumb.jpg ├── config.py ├── heroku.yml ├── install_unrar.sh ├── pyproject.toml ├── renovate.json ├── start.sh ├── unzipbot ├── __init__.py ├── __main__.py ├── helpers │ ├── database.py │ ├── start.py │ └── unzip_help.py ├── i18n │ ├── buttons.py │ ├── lang │ │ └── en.json │ └── messages.py └── modules │ ├── callbacks.py │ ├── commands.py │ └── ext_script │ ├── custom_thumbnail.py │ ├── ext_helper.py │ ├── metadata_helper.py │ └── up_helper.py └── uv.lock /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "secrets" 5 | 6 | [[analyzers]] 7 | name = "python" 8 | 9 | [analyzers.meta] 10 | runtime_version = "3.x.x" 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | **/__pycache__ 3 | *.session 4 | *.session-journal 5 | downloads/ 6 | Downloaded/ 7 | Thumbnails/ 8 | unknown_errors.txt 9 | unzip-bot.log 10 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | APP_ID= 2 | API_HASH= 3 | BOT_OWNER= 4 | BOT_TOKEN= 5 | MONGODB_DBNAME=Unzipper_Bot 6 | MONGODB_URL= 7 | LOGS_CHANNEL= 8 | UNZIPBOT_VERSION=7.3.0 9 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | We as members, contributors, and leaders pledge to make participation in our 5 | community a harassment-free experience for everyone, regardless of age, body 6 | size, visible or invisible disability, ethnicity, sex characteristics, gender 7 | identity and expression, level of experience, education, socio-economic status, 8 | nationality, personal appearance, race, religion, or sexual identity 9 | and orientation. 10 | 11 | We pledge to act and interact in ways that contribute to an open, welcoming, 12 | diverse, inclusive, and healthy community. 13 | 14 | ## Our Standards 15 | Examples of behavior that contributes to a positive environment for our 16 | community include : 17 | - Demonstrating empathy and kindness toward other people 18 | - Being respectful of differing opinions, viewpoints, and experiences 19 | - Giving and gracefully accepting constructive feedback 20 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 21 | - Focusing on what is best not just for us as individuals, but for the overall community 22 | 23 | Examples of unacceptable behavior include : 24 | - The use of sexualized language or imagery, and sexual attention or advances of any kind 25 | - Trolling, insulting or derogatory comments, and personal or political attacks 26 | - Public or private harassment 27 | - Publishing others' private information, such as a physical or email address, without their explicit permission 28 | - Other conduct which could reasonably be considered inappropriate in a professional setting 29 | 30 | ## Enforcement Responsibilities 31 | Community leaders are responsible for clarifying and enforcing our standards of 32 | acceptable behavior and will take appropriate and fair corrective action in 33 | response to any behavior that they deem inappropriate, threatening, offensive, 34 | or harmful. 35 | 36 | Community leaders have the right and responsibility to remove, edit, or reject 37 | comments, commits, code, wiki edits, issues, and other contributions that are 38 | not aligned to this Code of Conduct, and will communicate reasons for moderation 39 | decisions when appropriate. 40 | 41 | ## Scope 42 | This Code of Conduct applies within all community spaces, and also applies when 43 | an individual is officially representing the community in public spaces. 44 | Examples of representing our community include using an official e-mail address, 45 | posting via an official social media account, or acting as an appointed 46 | representative at an online or offline event. 47 | 48 | ## Enforcement 49 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 50 | reported to the community leaders responsible for enforcement at 51 | [`codeofconduct@edm115.dev`](mailto:codeofconduct@edm115.dev). 52 | All complaints will be reviewed and investigated promptly and fairly. 53 | 54 | All community leaders are obligated to respect the privacy and security of the 55 | reporter of any incident. 56 | 57 | ## Enforcement Guidelines 58 | 59 | Community leaders will follow these Community Impact Guidelines in determining 60 | the consequences for any action they deem in violation of this Code of Conduct : 61 | 62 | ### 1. Correction 63 | **Community Impact** : Use of inappropriate language or other behavior deemed 64 | unprofessional or unwelcome in the community. 65 | **Consequence** : A private, written warning from community leaders, providing 66 | clarity around the nature of the violation and an explanation of why the 67 | behavior was inappropriate. A public apology may be requested. 68 | 69 | ### 2. Warning 70 | **Community Impact** : A violation through a single incident or series 71 | of actions. 72 | **Consequence** : A warning with consequences for continued behavior. No 73 | interaction with the people involved, including unsolicited interaction with 74 | those enforcing the Code of Conduct, for a specified period of time. This 75 | includes avoiding interactions in community spaces as well as external channels 76 | like social media. Violating these terms may lead to a temporary or 77 | permanent ban. 78 | 79 | ### 3. Temporary Ban 80 | **Community Impact** : A serious violation of community standards, including 81 | sustained inappropriate behavior. 82 | **Consequence** : A temporary ban from any sort of interaction or public 83 | communication with the community for a specified period of time. No public or 84 | private interaction with the people involved, including unsolicited interaction 85 | with those enforcing the Code of Conduct, is allowed during this period. 86 | Violating these terms may lead to a permanent ban. 87 | 88 | ### 4. Permanent Ban 89 | **Community Impact** : Demonstrating a pattern of violation of community 90 | standards, including sustained inappropriate behavior, harassment of an 91 | individual, or aggression toward or disparagement of classes of individuals. 92 | **Consequence** : A permanent ban from any sort of public interaction within 93 | the community. 94 | 95 | ## Attribution 96 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 97 | version 2.0, available at 98 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 99 | 100 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 101 | enforcement ladder](https://github.com/mozilla/diversity). 102 | 103 | [homepage]: https://www.contributor-covenant.org 104 | 105 | For answers to common questions about this code of conduct, see the FAQ at 106 | https://www.contributor-covenant.org/faq. Translations are available at 107 | https://www.contributor-covenant.org/translations. 108 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | buy_me_a_coffee: edm115 2 | custom: ["https://paypal.me/8EDM115", "https://t.me/EDM115bots/698"] 3 | github: EDM115 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report any bug you found 3 | title: "[BUG] " 4 | labels: ["bug"] 5 | assignees: 6 | - EDM115 7 | body: 8 | - type: textarea 9 | id: bug_description 10 | attributes: 11 | label: Describe the bug 12 | description: A clear and concise description of what the bug is. 13 | - type: textarea 14 | id: steps_to_reproduce 15 | attributes: 16 | label: Steps to reproduce 17 | description: Steps to reproduce the behavior 18 | placeholder: | 19 | 1. Do this 20 | 2. Then that 21 | 3. See error 22 | - type: textarea 23 | id: expected_behavior 24 | attributes: 25 | label: Expected behavior 26 | description: A clear and concise description of what you expected to happen. 27 | - type: textarea 28 | id: screenshots 29 | attributes: 30 | label: Screenshots 31 | description: If applicable, add screenshots to help explain your problem. 32 | - type: textarea 33 | id: additional_context 34 | attributes: 35 | label: Additional context 36 | description: Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | title: "[FEATURE REQUEST] " 4 | labels: ["enhancement"] 5 | assignees: 6 | - EDM115 7 | body: 8 | - type: textarea 9 | id: feature_description 10 | attributes: 11 | label: Is your feature request related to a problem ? Please describe 12 | description: A clear and concise description of what the problem is. 13 | placeholder: I'm always frustrated when [...] 14 | - type: textarea 15 | id: solution 16 | attributes: 17 | label: Describe the solution you'd like 18 | description: A clear and concise description of what you want to happen. 19 | placeholder: | 20 | I would like to have a feature that does this and that 21 | - type: textarea 22 | id: alternatives 23 | attributes: 24 | label: Describe alternatives you've considered 25 | description: A clear and concise description of any alternative solutions or features you've considered. 26 | - type: textarea 27 | id: additional_context 28 | attributes: 29 | label: Additional context 30 | description: Add any other context or screenshots about the feature request here. 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | ## Changes 5 | 6 | - 7 | 8 | ## Related issues 9 | 10 | 11 | ## Impact 12 | 13 | 14 | ## Extra information 15 | 16 | 17 | ## Checklist 18 | - [ ] The name of the PR is clear 19 | - [ ] The PR uses the right labels 20 | - [ ] The workflow tests have passed 21 | - [ ] My code is guaranteed to not break existing behavior 22 | - [ ] The code is properly formatted 23 | - [ ] The commits are somewhat clear 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | updates: 2 | - package-ecosystem: "pip" 3 | assignees: 4 | - "EDM115" 5 | commit-message: 6 | include: scope 7 | prefix: "[dependabot/pip] " 8 | directory: "/" 9 | labels: 10 | - "dependabot/pip" 11 | pull-request-branch-name: 12 | separator: "/" 13 | reviewers: 14 | - "EDM115" 15 | schedule: 16 | interval: "monthly" 17 | time: "08:00" 18 | timezone: "Europe/Paris" 19 | version: 2 20 | -------------------------------------------------------------------------------- /.github/workflows/authors.yml: -------------------------------------------------------------------------------- 1 | name: Update Authors 2 | 3 | on: 4 | # push: 5 | # branches: 6 | # - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | run: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: wow-actions/update-authors@v1 15 | with: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | sort: "commits" 18 | bots: true 19 | commit: "chore(authors) [skip ci]" 20 | -------------------------------------------------------------------------------- /.github/workflows/black-code-style.yml: -------------------------------------------------------------------------------- 1 | name: Renders the whole code using Black code style 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | black: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | with: 13 | persist-credentials: false 14 | fetch-depth: 0 15 | - name: Black-ify 16 | uses: psf/black@stable 17 | with: 18 | options: "--verbose" 19 | - name: Commit changes 20 | run: | 21 | d=`date '+%Y/%m/%dT%H:%M:%SZ'` 22 | git config --local user.email ${{ secrets.MAIL }} 23 | git config --local user.name ${{ secrets.USERNAME }} 24 | git add -A 25 | git commit -m "Code style changed to Black at ${d}" || echo "No changes to commit" 26 | - name: Create Pull Request 27 | uses: peter-evans/create-pull-request@v7 28 | with: 29 | commit-message: "Apply Black code style" 30 | title: "Apply Black code style" 31 | body: | 32 | This pull request is auto-generated by the Black Code Formatter GitHub action. 33 | Please review the changes before merging. 34 | labels: style 35 | branch: black-format-code 36 | assignees: EDM115 37 | reviewers: EDM115 38 | draft: true 39 | sign-commits: true 40 | -------------------------------------------------------------------------------- /.github/workflows/build-docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker Image 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Build the Docker image 13 | run: docker build . --file Dockerfile --tag unzip-bot:$(date +%s) 14 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | schedule: 5 | - cron: "0 4 * * 6" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | analyze: 10 | name: Analyze 11 | runs-on: ubuntu-latest 12 | permissions: 13 | actions: read 14 | contents: read 15 | security-events: write 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | language: [ 'python' ] 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Initialize CodeQL 27 | uses: github/codeql-action/init@v3 28 | with: 29 | languages: ${{ matrix.language }} 30 | queries: "security-and-quality" 31 | 32 | - name: Perform CodeQL Analysis 33 | uses: github/codeql-action/analyze@v3 34 | with: 35 | category: "/language:${{matrix.language}}" 36 | -------------------------------------------------------------------------------- /.github/workflows/contributors.yml: -------------------------------------------------------------------------------- 1 | name: Contributors 2 | 3 | on: 4 | schedule: 5 | - cron: "0 8 * * 0" 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | contributors: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: wow-actions/contributors-list@v1 15 | with: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | svgPath: CONTRIBUTORS.svg 18 | sort: true 19 | round: true 20 | truncate: 0 21 | affiliation: "all" 22 | includeBots: true 23 | commitMessage: "chore(contributors) [skip ci]" 24 | -------------------------------------------------------------------------------- /.github/workflows/potential-duplicates.yml: -------------------------------------------------------------------------------- 1 | name: Potential Duplicates 2 | 3 | on: 4 | issues: 5 | types: [opened, edited] 6 | 7 | jobs: 8 | run: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: wow-actions/potential-duplicates@v1 12 | with: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | label: potential-duplicate 15 | state: all 16 | threshold: 0.6 17 | reactions: "eyes" 18 | comment: > 19 | Potential duplicates: {{#issues}} 20 | - [#{{ number }}] {{ title }} ({{ accuracy }}%) 21 | {{/issues}} 22 | -------------------------------------------------------------------------------- /.github/workflows/pr-triage-dummy.yml: -------------------------------------------------------------------------------- 1 | name: PR Triage Dummy 2 | 3 | on: 4 | pull_request_review: 5 | types: [submitted, dismissed] 6 | 7 | jobs: 8 | dummy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - run: echo "this is a dummy workflow that triggers a workflow_run; it's necessary because otherwise the repo secrets will not be in scope for externally forked pull requests" 12 | -------------------------------------------------------------------------------- /.github/workflows/pr-triage.yml: -------------------------------------------------------------------------------- 1 | name: PR Triage 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, closed, edited, reopened, synchronize, ready_for_review] 6 | workflow_run: 7 | workflows: ["PR Triage Dummy"] 8 | types: [requested] 9 | 10 | jobs: 11 | triage: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: wow-actions/pr-triage@v1 15 | with: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | WORKFLOW_ID: ${{ github.event.workflow_run.id }} 18 | -------------------------------------------------------------------------------- /.github/workflows/publish-docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker Image 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | push_to_registries: 10 | name: Push Docker image to GHCR & Docker Hub 11 | runs-on: ubuntu-latest 12 | permissions: 13 | packages: write 14 | contents: read 15 | steps: 16 | - name: Check out the repo 17 | uses: actions/checkout@v4 18 | 19 | - name: Log in to Docker Hub 20 | uses: docker/login-action@v3 21 | with: 22 | username: ${{ secrets.DOCKER_USERNAME }} 23 | password: ${{ secrets.DOCKER_TOKEN }} 24 | 25 | - name: Log in to the GitHub Container registry 26 | uses: docker/login-action@v3 27 | with: 28 | registry: ghcr.io 29 | username: ${{ github.actor }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Lowercase the repo name 33 | run: | 34 | echo "REPO_L=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}" 35 | 36 | - name: Extract metadata (tags, labels) for Docker 37 | id: meta 38 | uses: docker/metadata-action@v5 39 | with: 40 | images: | 41 | edm115/unzip-bot 42 | ghcr.io/${{ github.repository }} 43 | 44 | - name: Build and push Docker images 45 | uses: docker/build-push-action@v6 46 | with: 47 | context: . 48 | push: true 49 | tags: ${{ steps.meta.outputs.tags }},edm115/unzip-bot:latest,ghcr.io/${{ env.REPO_L }}:latest 50 | labels: ${{ steps.meta.outputs.labels }} 51 | -------------------------------------------------------------------------------- /.github/workflows/unfurl-links.yml: -------------------------------------------------------------------------------- 1 | name: Unfurl Links 2 | 3 | on: 4 | # issues: 5 | # types: [opened, edited] 6 | # issue_comment: 7 | # types: [created, edited] 8 | # pull_request: 9 | # types: [opened, edited] 10 | workflow_dispatch: 11 | 12 | jobs: 13 | run: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: wow-actions/unfurl-links@v1 17 | with: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | raw: false 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Environments 7 | .env 8 | .venv 9 | env/ 10 | venv/ 11 | .ruff_cache 12 | 13 | # SonarQube 14 | .scannerwork/ 15 | 16 | # Secrets 17 | *.session 18 | *.session-journal 19 | 20 | # Internal directories and files 21 | downloads/ 22 | Downloaded/ 23 | Thumbnails/ 24 | unknown_errors.txt 25 | unzip-bot.log 26 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12.10 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "atishay-jain.all-autocomplete", 4 | "aaron-bond.better-comments", 5 | "britesnow.vscode-toggle-quotes", 6 | "charliermarsh.ruff", 7 | "foxundermoon.shell-format", 8 | "ibm.output-colorizer", 9 | "meezilla.json", 10 | "mikestead.dotenv", 11 | "ms-azuretools.vscode-docker", 12 | "ms-python.debugpy", 13 | "ms-python.python", 14 | "ms-python.vscode-pylance", 15 | "ninoseki.vscode-mogami", 16 | "nhoizey.gremlins", 17 | "oliversturm.fix-json", 18 | "tamasfe.even-better-toml", 19 | "timonwong.shellcheck", 20 | "visualstudioexptteam.vscodeintellicode", 21 | "yzhang.markdown-all-in-one", 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[dockerfile]": { 3 | "editor.defaultFormatter": "ms-azuretools.vscode-docker" 4 | }, 5 | "[json]": { 6 | "editor.defaultFormatter": "vscode.json-language-features" 7 | }, 8 | "[markdown]": { 9 | "editor.defaultFormatter": "yzhang.markdown-all-in-one" 10 | }, 11 | "[python]": { 12 | "editor.defaultFormatter": "ms-python.black-formatter" 13 | }, 14 | "debug.inlineValues": "on", 15 | "debug.showVariableTypes": true, 16 | "debugpy.showPythonInlineValues": true, 17 | "diffEditor.experimental.useTrueInlineView": true, 18 | "diffEditor.hideUnchangedRegions.enabled": true, 19 | "diffEditor.ignoreTrimWhitespace": false, 20 | "diffEditor.maxComputationTime": 0, 21 | "docker.contexts.showInStatusBar": true, 22 | "docker.languageserver.diagnostics.instructionCasing": "error", 23 | "docker.languageserver.diagnostics.instructionCmdMultiple": "error", 24 | "docker.languageserver.diagnostics.instructionEntrypointMultiple": "error", 25 | "docker.languageserver.diagnostics.instructionJSONInSingleQuotes": "error", 26 | "docker.languageserver.diagnostics.instructionWorkdirRelative": "error", 27 | "editor.experimentalEditContextEnabled": true, 28 | "editor.formatOnPaste": false, 29 | "editor.formatOnSave": false, 30 | "editor.formatOnType": false, 31 | "editor.inlayHints.maximumLength": 0, 32 | "editor.inlineSuggest.enabled": true, 33 | "editor.inlineSuggest.showToolbar": "always", 34 | "editor.largeFileOptimizations": false, 35 | "editor.linkedEditing": true, 36 | "editor.maxTokenizationLineLength": 1000000, 37 | "editor.occurrencesHighlightDelay": 100, 38 | "editor.stickyTabStops": true, 39 | "editor.suggestSelection": "first", 40 | "editor.tabSize": 2, 41 | "editor.wordWrap": "on", 42 | "evenBetterToml.schema.associations": { 43 | "pyproject.toml": "https://json.schemastore.org/pyproject.json" 44 | }, 45 | "explorer.fileNesting.enabled": true, 46 | "explorer.fileNesting.expand": true, 47 | "explorer.fileNesting.patterns": { 48 | ".env": ".python-version, config.py", 49 | ".gitignore": ".deepsource.toml, .whitesource, AUTHORS, CONTRIBUTORS.svg, renovate.json", 50 | "Dockerfile": ".dockerignore, pyproject.toml, uv.lock", 51 | "heroku.yml": "app.json", 52 | "package.json": "package-lock.json, pnpm*", 53 | "README.md": "LICENSE, CHANGELOG.md, CreateMongoDB.md, bot_thumb.jpg", 54 | "start.sh": "install_unrar.sh" 55 | }, 56 | "files.autoSave": "afterDelay", 57 | "files.eol": "\n", 58 | "files.insertFinalNewline": true, 59 | "fixJson.indentationSpaces": 2, 60 | "git.allowForcePush": true, 61 | "git.autofetch": true, 62 | "git.confirmSync": false, 63 | "git.defaultBranchName": "master", 64 | "git.enableSmartCommit": true, 65 | "git.fetchOnPull": true, 66 | "git.ignoreLimitWarning": true, 67 | "git.openRepositoryInParentFolders": "never", 68 | "git.pullBeforeCheckout": true, 69 | "git.statusLimit": 0, 70 | "gremlins.showInProblemPane": true, 71 | "markdown-preview-enhanced.enableExtendedTableSyntax": true, 72 | "markdown-preview-enhanced.enableHTML5Embed": true, 73 | "markdown-preview-enhanced.enableScriptExecution": true, 74 | "markdown-preview-enhanced.enableTypographer": true, 75 | "markdown.extension.print.absoluteImgPath": false, 76 | "markdown.extension.toc.updateOnSave": false, 77 | "markdown.validate.enabled": true, 78 | "python.analysis.aiCodeActions": { 79 | "generateDocstring": true, 80 | "generateSymbol": true, 81 | "implementAbstractClasses": true 82 | }, 83 | "python.analysis.autoFormatStrings": true, 84 | "python.analysis.autoImportCompletions": true, 85 | "python.analysis.completeFunctionParens": true, 86 | "python.analysis.importFormat": "relative", 87 | "python.analysis.inlayHints.callArgumentNames": "all", 88 | "python.analysis.inlayHints.functionReturnTypes": true, 89 | "python.analysis.inlayHints.pytestParameters": true, 90 | "python.analysis.inlayHints.variableTypes": true, 91 | "python.analysis.languageServerMode": "full", 92 | "python.analysis.typeCheckingMode": "standard", 93 | "python.analysis.supportDocstringTemplate": true, 94 | "python.analysis.supportRestructuredText": true, 95 | "python.createEnvironment.trigger": "off", 96 | "python.missingPackage.severity": "Warning", 97 | "python.terminal.focusAfterLaunch": true, 98 | "python.terminal.shellIntegration.enabled": true, 99 | "scm.defaultViewMode": "tree", 100 | "scm.graph.badges": "all", 101 | "scm.inputFontFamily": "editor", 102 | "scm.workingSets.enabled": true, 103 | "search.defaultViewMode": "tree", 104 | "search.quickAccess.preserveInput": true, 105 | "togglequotes.chars": [ 106 | "'", 107 | "\"", 108 | "`" 109 | ] 110 | } 111 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "checkRunSettings": { 3 | "displayMode": "diff", 4 | "useMendCheckNames": true, 5 | "vulnerableCheckRunConclusionLevel": "failure" 6 | }, 7 | "issueSettings": { 8 | "assignees": [ 9 | "EDM115" 10 | ], 11 | "customLabels": [ 12 | "Mend: Security" 13 | ], 14 | "issueType": "DEPENDENCY", 15 | "minSeverityLevel": "LOW" 16 | }, 17 | "python": { 18 | "installVirtualenv": true, 19 | "invokePipAsModule": true, 20 | "path": "python3.12" 21 | }, 22 | "scanSettings": { 23 | "baseBranches": [ 24 | "master" 25 | ], 26 | "configMode": "AUTO", 27 | "cloneSubmodules": true 28 | }, 29 | "remediateSettings": { 30 | "enableRenovate": true, 31 | "workflowRules": { 32 | "enabled": true 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | EDM115 , , <82015596+edm115@users.noreply.github.com> 2 | renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 3 | deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com> 4 | Restyled.io 5 | stacksharebot 6 | dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7 | github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 8 | DeepSource Bot 9 | mend-bolt-for-github[bot] <42819689+mend-bolt-for-github[bot]@users.noreply.github.com> 10 | stack-file[bot] <147888899+stack-file[bot]@users.noreply.github.com> 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 |
2 | unzip-bot 3 | 4 | # unzip-bot • Changelog 5 | You will find here all the changes made with each version, in antichronological order 6 | 7 |
8 | 9 | > [!NOTE] 10 | > Updates versions follow semantic versioning (`vX.Y.Z`), where `X` stands for a major change and a lot of new features, `Y` for some new features and bug fixes, `Z` for testing stuff and undebugged things 11 | 12 | --- 13 | 14 | ## v7 15 | 16 |
17 | Click to expand 18 | 19 | ### v7.3.0 *(indev)* 20 | - Added VS Code settings 21 | - Use [uv](https://docs.astral.sh/uv/) 22 | - Use [Ruff](https://docs.astral.sh/ruff/) instead of Black 23 | - Use a `pyproject.toml` file 24 | - Simplifies the way we pass data to the `Messages.get()` i18n function, allows to use named arguments 25 | - All function calls now use named arguments, as far as Pylance reported them 26 | - Made `v7` the default branch 27 | 28 | ### v7.2.0 29 | - Switched from Kurigram to [pyrofork](https://github.com/Mayuri-Chan/pyrofork/) due to connection issues (I hate framework hopping) 30 | - Changed a bit the versioning scheme, see [#296 (comment)](https://github.com/EDM115/unzip-bot/issues/296#issuecomment-2727613676) 31 | - Bumped deps and Python 32 | 33 | ### v7.1.6a 34 | - Upgrade pyromod to v2+ 35 | - Let Kurigram handle floodwaits up to 2h 36 | 37 | ### v7.1.5a 38 | - Assume yes on all `unrar` commands 39 | - Try to switch to `pyroblack` again, reverts as it spams the logs with connexion issues 40 | - Correctly escapes non-Unicode characters in shell output and buttons filenames 41 | - Reduces the number of FloodWaits by reducing the number of messages sent/edited and not displaying an upload progressbar for files under 50 Mb, also handles FloodPremiumWait 42 | - Split RAR archives are finally supported 🥳 43 | - The start of the bot is finally async, although it hasn't been fully tested 44 | - Updated the Kurigram docs link 45 | 46 | ### v7.1.4a 47 | - Try the [KurimuzonAkuma fork of `pyrogram`](https://github.com/KurimuzonAkuma/pyrogram) 48 | - Bump `unrar` and `python` versions 49 | - Correctly display the unrar version on the build script 50 | - Already present thumbnails aren't downloaded on a start (useful when they are stored in a volume) 51 | 52 | ### v7.1.3a 53 | - Cleans the download dir on startup (especially helps when the bot is running with a volume attached) 54 | - Fixed issues with the lockfile that prevented the bot from starting 55 | - Removed an infinite loop that caused the bot to never go idle 56 | 57 | ### v7.1.2a 58 | - Creates a lock file on start 59 | - Deletes it in case of errors/shutting down 60 | - Restricts users from processing archives when the bot hasn't started yet 61 | 62 | ### v7.1.1a 63 | - Fixed `/exec` not being able to run properly 64 | - `/restart` now sends correctly the logs 65 | - Revert switch to `pyroblack` 66 | - Limit CPU usage too using `cpulimit` 67 | - Gets maxed at 80% of the current amount of cores for shell tasks 68 | - Ensures enough room is left to the bot process 69 | 70 | ### v7.1.0a 71 | - Stop using `return await` in async functions 72 | - Apply my very own code style on top of black 73 | - manually done yet 74 | - inspired by my heavily modified ESLint Stylistic config (https://github.com/EDM115/website/blob/master/eslint.config.js) 75 | - simply spaces out return, try, if, with, ... blocks to determine easier the branches that the program might take 76 | - Fixed a crash in the previous version 77 | - Use 80% of the available RAM on Heroku too instead of 100% 78 | - Shell commands are no longer using `shlex.join` to avoid several issues with path interpretation 79 | - Removal of duplicate logic 80 | - Video duration is properly parsed now, and the logic to catch non generated thumbnail is simplified 81 | - Migrate from `pyrogram` to `pyroblack` 82 | 83 | ### v7.0.3a 84 | - :warning: **Security fix** : The bot is no longer vulnerable to user attacks, see [GHSA-34cg-7f8c-fm5h / CVE-2024-53992](https://github.com/EDM115/unzip-bot/security/advisories/GHSA-34cg-7f8c-fm5h) for more info 85 | - Uses `asyncio.create_subprocess_shell()` instead of the hackish way that was present before 86 | - Uses `shlex` to sanitize user input for shell commands (file paths and archive passwords) 87 | - Moved `ERROR_MSGS` strings to a better place 88 | - Private functions have a more coherent naming 89 | - Fixed an oversight where `ffmpeg` commands were thread blocking 90 | 91 | ### v7.0.2a 92 | - The bot now stops properly when sent `SIGKILL` 93 | - Fixed an issue with formatting of strings 94 | - Implemented a memory limit on ran commands to avoid `R14` and `R15` errors on Heroku (first using `resource` then `ulimit`) 95 | - `/restart` and `/gitpull` now sends logs to the logs channel 96 | - Shell commands now uses `bash` instead of `sh` 97 | - Set a manual limit of RAM in Heroku (512 MB, can be manually changed) to avoid getting the limit being pulled from system info (wrong data as it gets it from the entire host) 98 | - Bumped aiohttp 99 | 100 | ### v7.0.1a 101 | - Strings processing is entirely redone 102 | - ALL strings are in JSON files, which will help with future translation 103 | - Only English supported for now 104 | - Deleted unused strings, moved plain text to the JSON, fixed grammar mistakes 105 | - Split buttons and messages processing 106 | - Added a default language that gets used for non-user tied strings and logs 107 | - Untranslated strings fall back to English (or default language) 108 | - String keys aren't definitives, hence why I haven't already started a French translation 109 | - Removed copyright mentions in files, added MIT notice in the start script 110 | - More commands that were restricted to the Bot Owner can now be run outside of DM, ex in the logs group (if they're not anonymous) 111 | - `7zip` is now installed from the `edge` repository to fix an issue with volumes creation 112 | - During the split of a file, it is now moved to a temp location to avoid filename clash 113 | - Users can finally cancel a task (see [#28](https://github.com/EDM115/unzip-bot/issues/28)), however it doesn't work perfectly for split archives download for example 114 | - The canceled task list is cleared at each restart and every 5 min 115 | 116 | ### v7.0.0a-herokufix 117 | - Added labels to the Docker image 118 | - Removed useless files and buildpacks for Heroku 119 | - Added `MONGODB_DBNAME` as an option for Heroku deployment 120 | - Fixed env vars issue in Heroku 121 | - Remove null and temp values for the thumbs db 122 | - Download the thumbs only after removing any previous tasks 123 | - Removed quotes from the `.env` 124 | 125 | ### v7.0.0a 126 | - Changed the versioning scheme, see [EDM115/unzip-bot@7.0.0a](https://github.com/EDM115/unzip-bot/releases/tag/7.0.0a) 127 | - First iteration of the massive refactor/rewrite 128 | - Applied isort and black code style 129 | - Fixed the AUTHORS file 130 | - Correctly name the project everywhere (`unzip-bot`) and renamed the module name to `unzipbot` 131 | - The logs file name changed from `unzip-log.txt` to `unzip-bot.log` 132 | - Switched from Arch Linux to Alpine Linux for the Docker image 133 | - The image size now weighs 294 MB instead of 1.87 GB 134 | - We use `7-zip` instead of `p7zip`, and we build `unrar` from source 135 | - Extra dependencies (like `g++`, `gcc` and `make`) are in a separate layer so they're not bundled in the end 136 | - Special handling for rar files 137 | - Temporarily fixes `FILE_REFERENCE_EXPIRED` errors when retrieving thumbnails 138 | - Actually handle `SIGTERM` 139 | 140 |
141 | 142 | --- 143 | 144 | ## v6 145 | 146 |
147 | Click to expand 148 | 149 | ### V6.3.5 150 | - Fixed a Docker crash due to the timezone not being set 151 | - Thumbnails now use Telegram file IDs and are no longer uploaded to telegra.ph. Backward compatibility is ensured for existing thumbnails 152 | - Added some Actions 153 | - Better Docker instructions 154 | - Properly access values from dicts with `get()` instead of `[]` 155 | - Added a `.dockerignore` file 156 | - Updated dependencies (aiohttp, dnspython, motor, Pillow, psutil) 157 | 158 | ### v6.3.4 159 | - Applied Black code style :black_heart: 160 | - Sends the logs to log channel when shutting down 161 | - Uses `shutil.move` instead of `os.rename` to move files (useful when 2 paths aren't on the same disk, ex Docker volumes) 162 | - Simplified the Dockerfile (less steps => less layers) 163 | - Added a pre-filled `.env`, and automatically load its vars if they aren't empty, else display a warning 164 | - Correctly handle spaces in file paths, fixing issues with ffmpeg and other command-line utilities 165 | - Display the entire file path on a file caption, instead of just the filename 166 | - Don't trigger doc/url process when using `/exec` and `/eval` 167 | - Added the `/privacy` command 168 | - The logs channel can now be an username, and we may have fixed an issue with Pyrogram being so old that it can't see 64-bits channel IDs 169 | - `/eval` and `/exec` now don't format the output when writing to a file 170 | - Deleted VIP related commands and strings for now 171 | - The DB collection name can be customized (useful when multiple bots run on the same DB but needs a different collection, ex not to share the ongoing tasks list) 172 | - The upload list buttons are hidden when uploading a file, assuring that no user spam click 173 | - The check for tasks running for more time than expected no longer relies on a `while True` + `asyncio.sleep`, but on an `aiocron` job 174 | - When rebooting, the timestamps sent to the owner are now readable 175 | - The video duration is now properly parsed (no more 0s videos) and the thumbnail is no longer generated from 0s but rather midway through the video 176 | - Several code improvements (style and bug-risk mainly) 177 | - Uses `ast.literal_eval` instead of `eval` for security reasons, catches properly most exceptions 178 | - Stopped using the deprecated `cgi` module and now gets filename from headers with `email.parser` 179 | - Updated the `.gitignore` 180 | - Sorted imports 181 | - When pushing a tagged image, it is now also pushed as `latest` 182 | - Updated Python runtime from 3.12.1 to 3.12.4 183 | - Updated dependencies (aiofiles, aiohttp, dnspython, GitPython, motor, Pillow, psutil, requests, unzip-http), added aiocron 184 | 185 | ### v6.3.3 186 | - Added support for PKG archives 187 | - ICO aren't treated as images anymore 188 | - When we upload a picture, either catch `PhotoExtInvalid` if it isn't meant to be uploaded as a picture 189 | or `PhotoSaveFileInvalid` if the picture is too big for Telegram 190 | - Added M4A and ALAC as Audio files 191 | - Reduced the number of Docker layers 192 | - Bumped the number of concurrent tasks to 75 193 | - Removed useless files and imports 194 | - Updated the licence years 195 | - Updated the gh bot's config files 196 | - Added ffmpeg buildpack 197 | - Updated Python runtime from 3.11.5 to 3.12.1 198 | - SIGTERM partial handling 199 | - New feature : if you Upload all, you won't get hundreds of notifications ! Now the bot sends the files silently and sends one notification when everything's uploaded 200 | - New password for testing archives 201 | - Cap the resources to avoid exceeding quotas 202 | - Don't upload files from MacOS archives (`.DS_Store` & `__MACOSX`) 203 | - Fixed TAR archives being broken (basically a `.tar.gz` would only upload the `.tar` inside) 204 | - Archives are no longer renamed to "archive_from_ID.ext" 205 | - Added `/eval` and `/exec` commands for Owner + `aexec()` function (thanks yash-dk and KangersHub, yoinked from https://github.com/KangersHub/TorToolkitX) 206 | - Audio files with media tags are uploaded with their tags 207 | - Tasks aren't processed if there is less than 5% disk space available 208 | - All ongoing tasks are removed instantly instead of one by one 209 | - Updated dependencies (GitPython, Pillow, aiohttp, psutil, gitdb, motor) 210 | 211 | ### v6.3.2 212 | - Fixed thumbnails not being saved 213 | - Premium related stuff is moved to its own branch (buggy so yes) 214 | - Fixed files being nearly all the time not uploaded 215 | - Better logging 216 | - Added [Mend Bolt](https://github.com/marketplace/whitesource-bolt) 217 | - Downgraded pyromod to 1.5 again (too many errors, I know they had been fixed in 2.1.0 but still) 218 | - Client specification in decorators instead of global `@Client` 219 | - New maintenance logic 220 | - Attempt to support files sent as TG links (may fail for topics, inaccessible chats and forward-restricted files) 221 | 222 | ### v6.3.1 223 | - Finally fixed [#133](https://github.com/EDM115/unzip-bot/issues/133) 224 | - Attempt to create a premium user to upload +2Gb files 225 | - Added `/maintenance` 226 | 227 | ### v6.3.0 228 | - Ongoing tasks are removed from the database after a restart 229 | - Added a new command : `/cleantasks` 230 | - Finally upgraded pyromod to v2 231 | - Upgraded Python from 3.11.3 to 3.11.5 232 | - Removed any trace of bayfiles upload since the service is dead 233 | - Support for `.partx.rar` split archives 234 | - Download files in 10 Mb chunks instead of 5 Mb 235 | - Added maintenance on DB 236 | - Added VIP methods in DB + implementation of no-restrictions for VIP ([#205](https://github.com/EDM115/unzip-bot/issues/205)) 237 | 238 | ### v6.2.4 239 | - Attempt to add some URL parsers (fail) 240 | - Even more refactor 241 | - Split files can be renamed 242 | - URLs are checked before extracting 243 | - If a thumbnail fails to be uploaded to telegra.ph, the error message is no longer saved in the db (and on download, non url strings are skipped) 244 | - `/broadcast` now shows how many users had been processed 245 | 246 | ### v6.2.3 247 | - Fixes minor errors on strings 248 | - Closes a lot of issues opened by DeepSource (mostly style) 249 | - Added a task limit (configurable in `config.py`) 250 | - FloodWait is now handled correctly everywhere 251 | - The bot is no longer blocking any task (finally) 252 | 253 | ### v6.2.2 254 | - Bugfix : No longer use `subprocess.communicate()`, as it's thread blocking 255 | - All strings are in `bot_data.py`, hope this should ease [#179](https://github.com/EDM115/unzip-bot/issues/179) 256 | - Even less thread block : use of `async for` and `yield` 257 | - Any file unreachable/with a size of 0B is skipped, thus avoiding the bot being stuck on an impossible task 258 | 259 | ### v6.2.1 260 | - :warning: **Security fix** : Merging files could lead to paths being swapped between users. It's now fixed 261 | 262 | ### v6.2.0 263 | - Added a new command : `/merge` (and `/done`) 264 | - Allows to merge split archives in .XXX format 265 | - Upload of thumbnails on telegra.ph now handles errors 266 | 267 | ### v6.1.0 268 | - URLs also show a progressbar + ETA when possible 269 | - Downloads are 28 times faster (not even kidding, we download in larger chunks) 270 | - Some databases are cleared upon restart 271 | - Attempt to implement [#137](https://github.com/EDM115/unzip-bot/issues/137) 272 | - New boot sequence 273 | 274 | ### v6.0.0 275 | - Dependencies update 276 | - tgz and zst archives are now supported 277 | - Thumbnail change tasks are now removed from DB after completion 278 | - Dockerfile has been updated : Addition of ffmpeg and venv 279 | - Uploading videos as media is fixed ! [#133](https://github.com/EDM115/unzip-bot/issues/133) 280 | - Added Docker instructions on the README 281 | - Added GitHub Actions for Docker publishing and deployment 282 | - Updated the FUNDING.yml 283 | - New command : `/donate`, plus donate button appears on `/start` and after a task is processed 284 | - Tell users that they can rate the bot after a task is processed 285 | - [#33](https://github.com/EDM115/unzip-bot/issues/33) is gone (no longer useless alerts) 286 | - ETA is now correct 287 | - Tried to add a way to cancel tasks, but it's not working 288 | - Files above 2 GB are now split 289 | 290 |
291 | 292 | --- 293 | 294 | ## v5 295 | 296 |
297 | Click to expand 298 | 299 | ### v5.3.1 300 | - Added `/gitpull` command to try the latest updates (removed at each restart), thanks Jusidama for the idea ! 301 | - `/delthumb` also works locally 302 | - Logs the boot time on database 303 | - Clears the logs on `/restart` (because in the end they're sent before actually restarting) 304 | - `/user2` now correctly format the link when an username is provided 305 | - Users are warned when the bot has restarted 306 | - So the ongoing tasks are also stored in the DB 307 | - And so `/stats` shows how many tasks are ongoing 308 | - X7 archives are now supported (probably misnamed zip ones) 309 | 310 | ### v5.3.0 311 | - Split archives are no longer processed (even .rar ones) 312 | - Sending videos as media worked but now instantly fails for an unknown reason 313 | - Heroku deployment file now complies with their drop of the free tier 314 | - Added `THUMB_DEL` buttons 315 | - Added ZIPX support 316 | - Added a Refresh button on `/stats` ([#143](https://github.com/EDM115/unzip-bot/issues/143)) 317 | 318 | ### v5.2.2 319 | - Happy new year 2023 🎉 320 | - Avoids double ban/unban 321 | - Fixed extensions recognition 322 | - Added a "Processing task" message 323 | 324 | ### v5.2.1 325 | - Added the website to `/help` 326 | - Python 3.10 -> 3.11 327 | - Added a new command : `/report` 328 | 329 | ### v5.2.0 330 | - Removal of the `personal_only` and `beta` branches, only `master` remains 331 | - Added permalink to the profile on `/user2` 332 | - Half refactor, a lot of errors and misuse of functions gone 333 | - Added `renovate[bot]` 334 | - Better new user formatting 335 | - ban/unban also acts on main `user_db` 336 | - Added support for IPSW archives on request 337 | 338 | ### v5.1.2 339 | - URL downloaded files finally have their original name 340 | - Split goes stonks (lie) 341 | - Prompting users to transload files I can't download 342 | - What happens on the terminal is now on the logs 343 | - Made `/listdir` and `/sendfile` for testing purposes 344 | - Added issue templates 345 | - `/delthumb` now also deletes it from the DB 346 | 347 | ### v5.1.1 348 | - **Huge code refactoring** 349 | - Little fixes 350 | - Still trying to split files 351 | - Thumbnail support is permanent 🥳 Redownloads them at every server restart 352 | - Clears correctly the thumbnails 353 | - FloodWait correctly handled 354 | - Bot starting happens on another file (so we can use async/await) 355 | 356 | ### v5.1.0 357 | - We fetch the file size *before* uploading 358 | - We try to split files above 2 GB (fail) 359 | 360 | ### v5.0.3 361 | - Added `/user2` and `/self` 362 | - Added ability to just change the thumbnail of the file (archive or not) 363 | - Also we can rename it 364 | 365 | ### v5.0.2 366 | - Heroku runtime shifted from Python 3.9.11 to 3.10.6 367 | - Added wheel for faster deployment 368 | - `/getthumbs` works 369 | 370 | ### v5.0.1 371 | - Made thumbnail support better (with buttons) 372 | - Saves the thumbnail URL (telegra.ph) to the DB 373 | - Buttons are side-by-side 374 | - Checks if sent file is actually an archive (so we stop processing PDF and MKV 😭) 375 | - Code style shifted to [Black](https://black.readthedocs.io/en/stable/) :black_heart: 376 | - Upgraded to Pyrogram v2 (finally) 377 | - The bot can process other things while extracting 378 | - Better password handling 379 | - Progressbar on uploads too 380 | - Uploads as media by default 381 | - Avoids split archives to be processed 382 | - Better `LOG_CHANNEL` verification 383 | 384 | ### v5.0.0 385 | - Added extensions list (for verification) 386 | - Medias are sent as native media 387 | - Fixed `ENTITY_BOUNDS_INVALID` error 388 | - Removed numpy as we don't use it 389 | - Added requests 390 | - Added development followup ([#38](https://github.com/EDM115/unzip-bot/issues/38)) 391 | - Uptime on `/stats` works correctly 392 | - Simpler buttons 393 | - Thumbnails on upload are officially supported 🥳 394 | - Commands updates (no `/setmode`, `/me` becomes `/info`, added `/stats` for everyone) 395 | 396 |
397 | 398 | --- 399 | 400 | ## v4 401 | 402 |
403 | Click to expand 404 | 405 | ### v4.5.0 406 | - Attempt to add `/merge` and `/cancel` commands + linked callbacks. Actually failed 407 | 408 | ### v4.4.5 409 | - The logs are better. Putting the text message *before* file, as it does with URL & replies to text message instead of file 410 | - Made a way more permissive regex for URL 411 | - Fixed exceptions on nearly all commands 412 | - Performing a `/restart` sends the logs automatically 413 | 414 | ### v4.4.4 415 | - Definitely fixed #NEW_USER 416 | - Once again tweaks on BayFiles 417 | 418 | ### v4.4.3 419 | - Way better handling of `check_logs()` on start 420 | - Fixed the #NEW_USER 421 | - Few changes on BayFiles upload 422 | 423 | ### v4.4.2 424 | - A lot of changes on BayFiles upload : 425 | - Errors sent to logs 426 | - Formatting the results with size, url and filename 427 | - Correct formatting of the errors 428 | - Created `get_cloud()`, will improve it to let the user choose the upload platform (bayfiles, anonfiles, …) 429 | 430 | ### v4.4.1 431 | - Instead of using external libraries, I use the official curl method from the BayFiles docs 432 | 433 | ### v4.4.0 434 | - If a file is above 2 Gb, it's uploaded to BayFiles instead 435 | - Better `get_files()` according to what Nexa made. Looks faster 436 | 437 | ### v4.3.4 438 | - Fixed crashes 439 | - Made `/stats` working for non owner, as requested in [#34](https://github.com/EDM115/unzip-bot/issues/34) 440 | 441 | ### v4.3.3 442 | - Added `/getthumbs`, which doesn't work 😅 443 | - User Name is better on the database (better formatting when a user joins) 444 | 445 | ### v4.3.2 446 | - Custom thumb made better + logging on it 447 | 448 | ### v4.3.1 449 | - This version crashes 450 | - Buggy thumbnails (files didn't upload due to this) 451 | - The thumbnail is resized according to Telegram API specifications 452 | - Thumbnails are saved to a separate folder 453 | - Created `thumb_exists()` 454 | 455 | ### v4.3.0 456 | - Created this changelog to track updates 457 | - Once again updated the uptime 458 | - Added numpy and Pillow in the requirements 459 | - Tried to have a thumbnail support. Nevertheless, it's removed at each restart. Will look for a Telegra.ph support 460 | 461 | ### v4.2.1 462 | - Major bug fixes 463 | 464 | ### v4.2.0 465 | - Added workaround for [#26](https://github.com/EDM115/unzip-bot/issues/26) 466 | - Attempt to make a really better ETA 467 | - Working around allowing user to cancel file/URL download (will look for the extracting process, bot can't reply while extracting) 468 | 469 | ### v4.1.1 470 | - Reduced amount of lines in logs (that was too much 💀) 471 | - Definitely fixed the bug of `v4.0.1` 472 | - Better texts 473 | - Keyboard now refreshes correctly after sending a file 474 | 475 | ### v4.1.0 476 | - Better handling of issue ([#2](https://github.com/EDM115/unzip-bot/issues/2)) + better usage of it (no longer systematically delete message) 477 | - Added `/dbexport`, `/commands`, `/admincmd` 478 | - Added `exec()` and `eval()`, but not usable now 479 | 480 | ### v4.0.7 481 | - Empty keyboard buttons are side to side 482 | 483 | ### v4.0.6 484 | - Major bug fixes 485 | 486 | ### v4.0.5 487 | - Tried to add date+time on logs filename. Can't actually do it because I will need to work with wildcards 488 | - Added logging for motor and asyncio 489 | - Added `/sendto` that works like `/broadcast` but to a single user. Works with chats and channels too. Will look for handling replies as well 490 | - You can use commands in more places 491 | 492 | ### v4.0.4 493 | - Major bug fixes 494 | - Upload count return `0` instead of `None` if it doesn't exist 495 | - Try to automatically perform a `/clean` when a task failed 496 | 497 | ### v4.0.3 498 | - Logs message now replies to the concerned archive. Better if multiple archives are processed at the same time 499 | - Errors show up in logs 500 | - Created an empty keyboard where only Upload all & Cancel shows up 501 | - Fixed major bug : `REPLY_MARKUP_TOO_LONG` ([#2](https://github.com/EDM115/unzip-bot/issues/2)) 502 | - Try to close session (to fix [#4](https://github.com/EDM115/unzip-bot/issues/4)) 503 | 504 | ### v4.0.2 505 | - `/mode` finally works as expected *(previous behavior added users to banned db when they changed their upload mode, thus the command couldn't work. This huge bug is present in Nexa's repo)* 506 | - Created a `TimeFormatter()` with seconds as input 507 | - Created upload file count (buggy). Barely saves in DB + only shows up in logs when user selected upload all mode 508 | 509 | ### v4.0.1 510 | - Tried to fetch the `SIGTERM` signal 511 | - Trying to fix a bug where the User Id no longer shows up in logs 512 | 513 | ### v4.0.0 514 | - Uses logging instead of print 515 | - Bot sends start time to logs *(may send stop time as well, but I need to handle `SIGTERM` gracefully)* 516 | - `/restart` now works *(but not as expected. Instead of killing and restarting the process, it creates a subprocess that behaves the same way)* 517 | - Added `/logs` to send a `.txt` containing the logs to the owner 518 | 519 |
520 | 521 | --- 522 | 523 | ## v3 524 | 525 |
526 | Click to expand 527 | 528 | ### v3.3.4 529 | - More emojis 530 | - Created `/help` and `/about` from home text 531 | 532 | ### v3.3.3 533 | - Minor bug fixes 534 | 535 | ### v3.3.2 536 | - Fully upgraded `/stats` 537 | - Added `/redbutton`, `/restart`, `/cleanall`, `/addthumb`, `/delthumb` 538 | 539 | ### v3.3.1 540 | - Minor text changes 541 | 542 | ### v3.3.0 543 | - Password archives no longer show an empty upload button 544 | - Sends file downloaded from URL to logs 545 | 546 | ### v3.2.2 547 | - Added password warning in both user chat and logs 548 | 549 | ### v3.2.1 550 | - Less imports on `ext_helper.py` 551 | 552 | ### v3.2.0 553 | - Added another `HumanBytes()` and functions for a better ETA 554 | - BIG CHANGE : the bot no longer hangs when a password protected archive is extracted normally ! 🥳 555 | 556 | ### v3.1.1 557 | - Fixed some errors 558 | 559 | ### v3.1.0 560 | - Captions *really* works now 561 | 562 | ### v3.0.2 563 | - Added precise versioning of packages (due to Pyrogram 2 release) 564 | - Python 3.9.11 as default runtime 565 | - Removed mentions of Nexa since this project is taking a different direction 566 | 567 | ### v3.0.1 568 | - Added `/me`, `/user`, `/db`, `/dbdive` 569 | 570 | ### v3.0.0 571 | - Added filename in description while uploading 572 | - Sending password of archive in logs 573 | 574 |
575 | 576 | --- 577 | 578 | ## v2 579 | 580 |
581 | Click to expand 582 | 583 | ### v2.0.0 584 | - Same as `v1.0.0`, but with text changed, typos fixed, mentions of me, more emojis, less formatting, … 585 | - Changed license from GPL 3.0 to MIT since the entire project structure is becoming different and the code is no longer the same 586 | 587 |
588 | 589 | --- 590 | 591 | ## v1 592 | 593 |
594 | Click to expand 595 | 596 | ### v1.0.0 597 | - Consider this as the [original work of Nexa](https://github.com/EDM115/unzip-bot#license--copyright-%EF%B8%8F) 598 | 599 |
600 | -------------------------------------------------------------------------------- /CreateMongoDB.md: -------------------------------------------------------------------------------- 1 | # How to create a MongoDB URL in 10min ? 2 | 3 | 1. Go to [MongoDB website](https://mongodb.com/cloud/atlas/register) 4 | *(skip steps 2 and 3 if you already have an account, just create a new cluster)* 5 | 1. On the account setup page, choose wisely your Organization name and Project name (you can't change that). Select Python as preferred language 6 | 2. Choose `Create a cluster` on the `Shared Clusters` category (the free one) 7 | 3. Choose `Azure` as provider and `Netherlands` as region, then click on `Create Cluster` (it takes 1-5 minutes, but don't close the window !) 8 | 4. When it's done, click on `Network Access` on left panel 9 | 1. `Add IP Address` 10 | 2. `Allow access from anywhere` (because Heroku IP addresses changes everytime) 11 | 3. `Confirm` (it can take up to 2 minutes) 12 | 5. Click on `Clusters` on left panel 13 | 1. `Connect` 14 | 2. Create a Database User (remember the credentials you choose !) 15 | 3. `Choose a connection method` 16 | 4. `Connect with your application` 17 | 5. Choose `Python` as Driver 18 | 6. Choose `3.6 or later` as Version 19 | 7. Copy the URL 20 | 6. Replace `` with the one you chosen at step 5.ii 21 | 22 | ## You're done :partying_face: 23 | ### Now use that as `MONGODB_URL` 24 | > [!NOTE] 25 | > It should look like : `mongodb+srv://username:password@cluster-name.abcde.mongodb.net/?retryWrites=true&w=majority` 26 | > You can strip the DB name here, it's set on the `MONGODB_DBNAME` env var 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-alpine AS build 2 | 3 | ENV UV_INSTALL_DIR="/uv" 4 | 5 | RUN apk update && \ 6 | apk add --no-cache \ 7 | bash \ 8 | curl \ 9 | g++ \ 10 | gcc \ 11 | libffi-dev \ 12 | make \ 13 | musl-dev && \ 14 | curl -LsSf https://astral.sh/uv/install.sh | sh 15 | 16 | SHELL ["/bin/bash", "-c"] 17 | 18 | ENV PATH="$UV_INSTALL_DIR:$PATH" 19 | 20 | WORKDIR /tmp 21 | 22 | COPY pyproject.toml /tmp/pyproject.toml 23 | COPY uv.lock /tmp/uv.lock 24 | COPY install_unrar.sh /tmp/install_unrar.sh 25 | 26 | RUN uv sync --no-cache --locked && \ 27 | /tmp/install_unrar.sh 28 | 29 | FROM python:3.12-alpine 30 | 31 | ENV UV_INSTALL_DIR="/uv" 32 | ARG VERSION="7.3.0" 33 | 34 | LABEL org.opencontainers.image.authors="EDM115 " 35 | LABEL org.opencontainers.image.base.name="python:3.12-alpine" 36 | LABEL org.opencontainers.image.licenses="MIT" 37 | LABEL org.opencontainers.image.source="https://github.com/EDM115/unzip-bot.git" 38 | LABEL org.opencontainers.image.title="unzip-bot" 39 | LABEL org.opencontainers.image.url="https://github.com/EDM115/unzip-bot" 40 | LABEL org.opencontainers.image.version=${VERSION} 41 | 42 | RUN apk update && \ 43 | apk add --no-cache \ 44 | bash \ 45 | cgroup-tools \ 46 | cpulimit \ 47 | curl \ 48 | ffmpeg \ 49 | git \ 50 | tar \ 51 | tzdata \ 52 | util-linux \ 53 | zstd && \ 54 | apk add --no-cache 7zip --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main && \ 55 | curl -LsSf https://astral.sh/uv/install.sh | sh && \ 56 | ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime 57 | 58 | SHELL ["/bin/bash", "-c"] 59 | 60 | ENV PATH="$UV_INSTALL_DIR:/venv/bin:$PATH" 61 | ENV TZ=Europe/Paris 62 | 63 | WORKDIR /app 64 | 65 | COPY --from=build /tmp/.venv /venv 66 | COPY --from=build /usr/local/bin/unrar /tmp/unrar 67 | 68 | RUN git clone -b v7 https://github.com/EDM115/unzip-bot.git /app && \ 69 | install -m 755 /tmp/unrar /usr/local/bin && \ 70 | rm -rf /tmp/unrar && \ 71 | source /venv/bin/activate 72 | 73 | COPY .env /app/.env 74 | 75 | ENTRYPOINT ["/bin/bash", "/app/start.sh"] 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 EDM115 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # unzip-bot 4 | ## A Telegram bot to extract various types of archives 5 | 6 | unzip-bot 7 | 8 | [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/ruff) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![DeepSource](https://app.deepsource.com/gh/EDM115/unzip-bot.svg/?label=active+issues&show_trend=true&token=17SfwVx77dbrFlixtGdQsQNh)](https://app.deepsource.com/gh/EDM115/unzip-bot/?ref=repository-badge) 9 | 10 | [![unzip-bot analytics](https://repobeats.axiom.co/api/embed/5c857b55b42dd8235388093858b74341f6c679ac.svg)](https://github.com/EDM115/unzip-bot/pulse) 11 | 12 |
13 | 14 | > [!IMPORTANT] 15 | > The bot is undergoing an important rewrite. 16 | > Please be patient and wait a few months to get the unzip-bot in its full glory ! 17 | > Check [[ROADMAP] The future of unzip-bot (v7) (#296)](https://github.com/EDM115/unzip-bot/issues/296) to know more about the current development. 18 | > Check [the master branch](https://github.com/EDM115/unzip-bot/tree/master) to use the older version. 19 | 20 | --- 21 | 22 | ## Working bot :smiling_face_with_three_hearts: 23 | [@unzip_edm115bot](https://t.me/unzip_edm115bot) 24 | More info : [edm115.dev/unzip](https://edm115.dev/unzip) 25 | 26 | ## Features :eyes: 27 | ### User side 28 | - Extract all formats of archives like `rar`, `zip`, `7z`, `tar.gz`, … 29 | - Supports password protected archives 30 | - Able to process split archives (`.001`, `.partX.rar`, …) 31 | - Download files from links 32 | - Rename and set custom thumbnail for files 33 | - Uploads files as documents or media 34 | - Can report issues directly 35 | 36 | ### Admin side 37 | - Broadcast messages to all users or specific ones 38 | - Ban/unban users from using your bot 39 | - Get realtime stats of the bot usage, along an API 40 | - Ability to set sudo users 41 | - Restart simply the bot and pull updates in one command 42 | - Can eval and exec code directly from Telegram 43 | - Send logs in a custom channel/group + retrieve logs from the bot 44 | And much more :fire: Dive into the code to find out :hand_over_mouth: 45 | 46 | ## Config vars :book: 47 | - `APP_ID` - Your APP ID. Get it from [my.telegram.org](https://my.telegram.org) 48 | - `API_HASH` - Your API_HASH. Get it from [my.telegram.org](https://my.telegram.org) 49 | - `BOT_OWNER` - Your Telegram Account ID. Get it from [@MissRose_bot](https://t.me/MissRose_bot) (Start the bot and send `/info` command). 50 | - `BOT_TOKEN` - Bot Token of Your Telegram Bot. Get it from [@BotFather](https://t.me/BotFather) 51 | - `MONGODB_DBNAME` - *(optional)* A custom name for the MongoDB database, useful if you deploy multiple instances of the bot on the same account. Defaults to `Unzipper_Bot` 52 | - `MONGODB_URL` - Your MongoDB URL ([**tutorial here**](CreateMongoDB.md)) 53 | - `LOGS_CHANNEL` - Make a private channel and get its ID (search on Google if you don't know how to do). Using a group works as well, just add [`Rose`](https://t.me/MissRose_bot?startgroup=startbot), then send `/id` (In both cases, **make sure to add your bot to the channel/group as an admin !**) 54 | 55 | ## Commands :writing_hand: 56 | Copy-paste those to BotFather when he asks you for them 57 | ```text 58 | commands - Get commands list 59 | mode - Upload as Doc 📄 / Media 📺 60 | addthumb - Add custom thumbnail 61 | delthumb - Remove your thumbnail 62 | stats - Know if bot is overused 63 | clean - Cancel ongoing process 64 | help - In case you need 😭 65 | ``` 66 | 67 | ## Deploy :construction: 68 | Deploying is easy :smiling_face_with_three_hearts: You can deploy this bot in Heroku or in a VPS :heart: 69 | **Star :star2: Fork :fork_and_knife: and Deploy :outbox_tray:** 70 | 71 | #### The lazy way 72 | [![Deploy me :pleading_face:](https://www.herokucdn.com/deploy/button.svg)](https://www.heroku.com/deploy?template=https://github.com/EDM115/unzip-bot/tree/v7) 73 | (if you're in a fork, make sure to replace the template URL with your repo's one, also replace the URL in the Dockerfile) 74 | 75 | #### The easy way 76 | - Install [Docker](https://www.docker.com/) then restart your computer (if on Windows) 77 | ```bash 78 | git clone https://github.com/EDM115/unzip-bot.git && cd unzip-bot 79 | nano .env 80 | docker build -t edm115/unzip-bot . 81 | ``` 82 | - Open Docker Desktop, go on the Images tab, click on the Run button 83 | - On Optional settings, fill the env variables 84 | 85 | #### The nerdy way 86 | ```bash 87 | git clone https://github.com/EDM115/unzip-bot.git && cd unzip-bot 88 | nano .env 89 | docker build -t edm115/unzip-bot . 90 | docker run -d -v downloaded-volume-prod:/app/Downloaded -v thumbnails-volume-prod:/app/Thumbnails --env-file ./.env --name unzipbot edm115/unzip-bot 91 | ``` 92 | 93 | **DONE :partying_face: enjoy the bot !** Be sure to follow me on [GitHub](https://github.com/EDM115) and Star :star2: this repo to show some support :pleading_face: 94 | 95 | ## How to build after changes ? 96 | #### Trust GitHub Actions 97 | - Add new Actions secrets to the repo : 98 | - `DOCKER_USERNAME` : all in lowercase 99 | - `DOCKER_TOKEN` : one with all rights, here : https://hub.docker.com/settings/security 100 | - Go in Actions tab, 2 workflows are here for ya : 101 | - `Build Docker Image` : Check if it builds without errors 102 | - `Publish Docker Image` : Rebuild && publish 103 | 104 | #### Do it manually 105 | - Go in the repo's folder 106 | ```bash 107 | docker build --no-cache -t edm115/unzip-bot . 108 | docker run -d -v downloaded-volume:/app/Downloaded -v thumbnails-volume:/app/Thumbnails --env-file ./.env --network host --name unzip-bot-container edm115/unzip-bot 109 | docker start unzip-bot-container 110 | # if you want to check something 111 | docker exec -it unzip-bot-container sh 112 | docker logs unzip-bot-container 113 | # once you're done 114 | docker stop unzip-bot-container 115 | ``` 116 | - If you wanna publish : 117 | ```bash 118 | docker tag edm115/unzip-bot edm115/unzip-bot:latest 119 | ``` 120 | *(replace `edm115` with your docker hub username, `unzip-bot` with the repo's name and `latest` whith whatever you want)* 121 | ```bash 122 | docker login 123 | ``` 124 | *login and don't mind the errors* 125 | ```bash 126 | docker push edm115/unzip-bot:latest 127 | ``` 128 | *(same, replace accordingly)* 129 | 130 | ## Found a bug :bug: 131 | If you found a bug in this bot please open an [issue](https://github.com/EDM115/unzip-bot/issues) or report it on Telegram : [@EDM115](https://t.me/EDM115) 132 | Same if you have any feature request :wink: 133 | 134 | ## License & Copyright :cop: 135 | Copyright (c) 2022 - 2025 EDM115 136 | 137 | This unzip-bot repository is licensed under the [MIT License](https://github.com/EDM115/unzip-bot/blob/master/LICENSE) 138 | Enjoy copying and modifying, but always mention me 139 | 140 | • Inspired by Itz-fork/Nexa's work, but with additional features and bug fixes. 141 | This is a maintained repo of the [original](https://github.com/Itz-fork/Unzipper-Bot), props to him for the OG code 142 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "A Telegram bot to extract various types of archives", 3 | "env": { 4 | "API_HASH": { 5 | "description": "Your API_HASH from my.telegram.org", 6 | "required": true 7 | }, 8 | "APP_ID": { 9 | "description": "Your APP_ID from my.telegram.org", 10 | "required": true 11 | }, 12 | "BOT_OWNER": { 13 | "description": "Telegram ID of your account", 14 | "required": true 15 | }, 16 | "BOT_TOKEN": { 17 | "description": "Your bot token from @BotFather", 18 | "required": true 19 | }, 20 | "LOGS_CHANNEL": { 21 | "description": "ID of a channel, can also be a group", 22 | "required": true 23 | }, 24 | "MONGODB_DBNAME": { 25 | "description": "Your MongoDB database name. Leave it as default unless you make multiple deployments", 26 | "required": true, 27 | "value": "Unzipper_Bot" 28 | }, 29 | "MONGODB_URL": { 30 | "description": "Your MongoDB url, get it from https://www.mongodb.com/", 31 | "required": true 32 | } 33 | }, 34 | "formation": { 35 | "worker": { 36 | "quantity": 1, 37 | "size": "eco" 38 | } 39 | }, 40 | "keywords": [ 41 | "7z", 42 | "zip", 43 | "rar", 44 | "Telegram Bot", 45 | "unzip bot" 46 | ], 47 | "logo": "https://telegra.ph/file/d4ba24682e030fc58613f.jpg", 48 | "name": "unzip-bot", 49 | "repository": "https://github.com/EDM115/unzip-bot", 50 | "stack": "container", 51 | "success_url": "https://t.me/EDM115bots", 52 | "website": "https://edm115.dev/unzip" 53 | } 54 | -------------------------------------------------------------------------------- /bot_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EDM115/unzip-bot/a52e4e4f43f04d1e08e43c218dd50056edf590a6/bot_thumb.jpg -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import psutil 4 | 5 | 6 | class Config: 7 | APP_ID = int(os.environ.get("APP_ID")) 8 | API_HASH = os.environ.get("API_HASH") 9 | BASE_LANGUAGE = os.environ.get("BASE_LANGUAGE", default="en") 10 | BOT_TOKEN = os.environ.get("BOT_TOKEN") 11 | BOT_THUMB = f"{os.path.dirname(__file__)}/bot_thumb.jpg" 12 | BOT_OWNER = int(os.environ.get("BOT_OWNER")) 13 | # Default chunk size (0.005 MB → 1024*6) Increase if you need faster downloads 14 | CHUNK_SIZE = 1024 * 1024 * 10 # 10 MB 15 | DOWNLOAD_LOCATION = f"{os.path.dirname(__file__)}/Downloaded" 16 | IS_HEROKU = os.environ.get("DYNO", default="").startswith(prefix="worker.") 17 | LOCKFILE = "/tmp/unzipbot.lock" 18 | LOGS_CHANNEL = ( 19 | int(os.environ.get("LOGS_CHANNEL")) 20 | if os.environ.get("LOGS_CHANNEL").strip("-").isdigit() 21 | else os.environ.get("LOGS_CHANNEL") 22 | ) 23 | MAX_CONCURRENT_TASKS = 75 24 | MAX_MESSAGE_LENGTH = 4096 25 | MAX_CPU_CORES_COUNT = psutil.cpu_count(logical=False) 26 | MAX_CPU_USAGE = 80 27 | # 512 MB by default for Heroku, unlimited otherwise 28 | MAX_RAM_AMOUNT_KB = 1024 * 512 if IS_HEROKU else -1 29 | MAX_RAM_USAGE = 80 30 | MAX_TASK_DURATION_EXTRACT = 120 * 60 # 2 hours (in seconds) 31 | MAX_TASK_DURATION_MERGE = 240 * 60 # 4 hours (in seconds) 32 | # Files under that size will not display a progress bar while uploading 33 | MIN_SIZE_PROGRESS = 1024 * 1024 * 50 # 50 MB 34 | MONGODB_URL = os.environ.get("MONGODB_URL") 35 | MONGODB_DBNAME = os.environ.get("MONGODB_DBNAME", default="Unzipper_Bot") 36 | TG_MAX_SIZE = 2097152000 37 | THUMB_LOCATION = f"{os.path.dirname(__file__)}/Thumbnails" 38 | VERSION = os.environ.get("UNZIPBOT_VERSION", default="7.3.0") 39 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | worker: Dockerfile 4 | -------------------------------------------------------------------------------- /install_unrar.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # unrar build and install script 4 | # origin : https://github.com/linuxserver/docker-sabnzbd/blob/444da02e31823289c4d4ca6ab407442bf6719e94/Dockerfile#L28-L38 5 | # source : https://www.reddit.com/r/AlpineLinux/comments/13p4p5k/comment/jmrdr24/ 6 | # get unrar version : https://www.rarlab.com/rar_add.htm 7 | UNRAR_VERSION="7.1.6" 8 | 9 | echo "Installing unrar version: ${UNRAR_VERSION}" 10 | mkdir /tmp/unrar 11 | curl -o \ 12 | /tmp/unrar.tar.gz -L \ 13 | "https://www.rarlab.com/rar/unrarsrc-${UNRAR_VERSION}.tar.gz" 14 | 15 | tar xf \ 16 | /tmp/unrar.tar.gz -C \ 17 | /tmp/unrar --strip-components=1 18 | cd /tmp/unrar || exit 19 | 20 | make 21 | install -v -m755 unrar /usr/local/bin 22 | echo "unrar version: $(unrar -iver) installed successfully" 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "unzip-bot" 3 | version = "7.2.0" 4 | authors = [ 5 | { name="EDM115", email="unzip@edm115.dev" }, 6 | ] 7 | description = "A Telegram bot to extract various types of archives" 8 | dependencies = [ 9 | "aiocron==2.1", 10 | "aiofiles==24.1.0", 11 | "aiohttp==3.11.18", 12 | "base58check==1.0.2", 13 | "dnspython==2.7.0", 14 | "gitdb==4.0.12", 15 | "GitPython==3.1.44", 16 | "motor==3.7.0", 17 | "mutagen==1.47.0", 18 | "Pillow==11.2.1", 19 | "psutil==7.0.0", 20 | "pykeyboard==0.1.5", 21 | "pyrofork==2.3.61", 22 | "PyTgCrypto==1.2.9.2", 23 | "requests==2.32.3", 24 | "setuptools==80.1.0", 25 | "unzip-http==0.6", 26 | "wheel==0.46.1", 27 | ] 28 | requires-python = ">=3.10" 29 | readme = "README.md" 30 | license = {file = "LICENSE"} 31 | 32 | [project.scripts] 33 | format = "ruff format" 34 | install = "uv sync" 35 | install-pip = "pip install -U -r requirements.txt" 36 | lint = "ruff check" 37 | lint-fix = "ruff check --fix" 38 | run = "./start.sh" 39 | venv = "python -m venv .venv" 40 | 41 | [project.urls] 42 | "Homepage" = "https://github.com/EDM115/unzip-bot" 43 | "Bug Tracker" = "https://github.com/EDM115/unzip-bot/issues" 44 | "Funding" = "https://github.com/EDM115#support-me-" 45 | 46 | [tool.ruff] 47 | target-version = "py310" 48 | 49 | [tool.ruff.format] 50 | docstring-code-format = true 51 | line-ending = "lf" 52 | skip-magic-trailing-comma = true 53 | 54 | [tool.ruff.lint] 55 | logger-objects = ["unzipbot.LOGGER"] 56 | select = ["E", "F", "I", "W"] 57 | 58 | [tool.ruff.lint.isort] 59 | split-on-trailing-comma = false 60 | 61 | [tool.uv] 62 | compile-bytecode = true 63 | link-mode = "copy" 64 | 65 | [tool.uv.pip] 66 | compile-bytecode = true 67 | generate-hashes = true 68 | link-mode = "copy" 69 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "addLabels": [ 4 | "Mend: Renovate" 5 | ], 6 | "assignees": [ 7 | "EDM115" 8 | ], 9 | "cloneSubmodules": true, 10 | "commitBodyTable": true, 11 | "configMigration": true, 12 | "dependencyDashboard": true, 13 | "dependencyDashboardOSVVulnerabilitySummary": "unresolved", 14 | "draftPR": true, 15 | "enabled": true, 16 | "extends": [ 17 | "config:recommended" 18 | ], 19 | "ignoreDeps": [ 20 | "python" 21 | ], 22 | "labels": [ 23 | "Mend: Renovate" 24 | ], 25 | "osvVulnerabilityAlerts": true, 26 | "prConcurrentLimit": 0, 27 | "prHourlyLimit": 0, 28 | "reviewers": [ 29 | "EDM115" 30 | ], 31 | "semanticCommitScope": "dependencies", 32 | "semanticCommitType": "update", 33 | "semanticCommits": "enabled", 34 | "stopUpdatingLabel": "Mend: Stop updating", 35 | "timezone": "Europe/Paris" 36 | } 37 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo " 4 | 🔥 unzip-bot 🔥 5 | 6 | Copyright (c) 2022 - 2025 EDM115 7 | MIT License 8 | 9 | --> Join @EDM115bots on Telegram 10 | --> Follow EDM115 on Github 11 | " 12 | 13 | if [ -f .env ] && [[ ! "$DYNO" =~ ^worker.* ]]; then 14 | if grep -qE '^[^#]*=\s*("|'\''?)\s*\1\s*$' .env; then 15 | echo "Some required vars are empty, please fill them unless you're filling them somewhere else (ex : Heroku, Docker Desktop)" 16 | else 17 | while IFS='=' read -r key value; do 18 | if [[ ! $key =~ ^# && -n $key ]]; then 19 | export "$key=$value" 20 | fi 21 | done < .env 22 | fi 23 | fi 24 | 25 | exec python -m unzipbot 26 | -------------------------------------------------------------------------------- /unzipbot/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | from pyrogram import Client 5 | 6 | from config import Config 7 | 8 | boottime = time.time() 9 | plugins = dict(root="modules") 10 | 11 | unzipbot_client = Client( 12 | name="unzip-bot", 13 | bot_token=Config.BOT_TOKEN, 14 | api_id=Config.APP_ID, 15 | api_hash=Config.API_HASH, 16 | plugins=plugins, 17 | sleep_threshold=7200, 18 | max_concurrent_transmissions=3, 19 | ) 20 | 21 | logging.basicConfig( 22 | level=logging.INFO, 23 | handlers=[logging.FileHandler(filename="unzip-bot.log"), logging.StreamHandler()], 24 | format="%(asctime)s - %(levelname)s - %(name)s - %(threadName)s - %(message)s", 25 | ) 26 | 27 | LOGGER = logging.getLogger(__name__) 28 | 29 | logging.getLogger("asyncio").setLevel(logging.WARNING) 30 | logging.getLogger("aiohttp").setLevel(logging.WARNING) 31 | logging.getLogger("aiofiles").setLevel(logging.WARNING) 32 | logging.getLogger("dnspython").setLevel(logging.WARNING) 33 | logging.getLogger("GitPython").setLevel(logging.WARNING) 34 | logging.getLogger("motor").setLevel(logging.WARNING) 35 | logging.getLogger("Pillow").setLevel(logging.WARNING) 36 | logging.getLogger("psutil").setLevel(logging.WARNING) 37 | logging.getLogger("pyrogram").setLevel(logging.WARNING) 38 | logging.getLogger("requests").setLevel(logging.WARNING) 39 | -------------------------------------------------------------------------------- /unzipbot/__main__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import signal 4 | import time 5 | 6 | from pyrogram import idle 7 | 8 | from config import Config 9 | 10 | from . import LOGGER, unzipbot_client 11 | from .helpers.database import get_lang 12 | from .helpers.start import ( 13 | check_logs, 14 | dl_thumbs, 15 | remove_expired_tasks, 16 | set_boot_time, 17 | start_cron_jobs, 18 | ) 19 | from .i18n.messages import Messages 20 | 21 | messages = Messages(lang_fetcher=get_lang) 22 | 23 | 24 | async def async_shutdown_bot(): 25 | stoptime = time.strftime("%Y/%m/%d - %H:%M:%S") 26 | LOGGER.info(msg=messages.get(file="main", key="STOP_TXT", extra_args=stoptime)) 27 | 28 | tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] 29 | [task.cancel() for task in tasks] 30 | await asyncio.gather(*tasks, return_exceptions=True) 31 | 32 | try: 33 | await unzipbot_client.send_message( 34 | chat_id=Config.LOGS_CHANNEL, 35 | text=messages.get(file="main", key="STOP_TXT", extra_args=stoptime), 36 | ) 37 | 38 | with open(file="unzip-bot.log", mode="rb") as doc_f: 39 | try: 40 | await unzipbot_client.send_document( 41 | chat_id=Config.LOGS_CHANNEL, document=doc_f, file_name=doc_f.name 42 | ) 43 | except: 44 | pass 45 | except Exception as e: 46 | LOGGER.error( 47 | msg=messages.get(file="main", key="ERROR_SHUTDOWN_MSG", extra_args=e) 48 | ) 49 | finally: 50 | await unzipbot_client.stop() 51 | LOGGER.info(msg=messages.get(file="main", key="BOT_STOPPED")) 52 | 53 | 54 | def handle_stop_signals(signum, frame): 55 | LOGGER.info( 56 | msg=messages.get( 57 | file="main", 58 | key="RECEIVED_STOP_SIGNAL", 59 | extra_args=[signal.Signals(signum).name, signum, frame], 60 | ) 61 | ) 62 | loop = asyncio.get_event_loop() 63 | loop.create_task(coro=async_shutdown_bot()) 64 | 65 | 66 | def setup_signal_handlers(): 67 | loop = asyncio.get_event_loop() 68 | 69 | for sig in (signal.SIGINT, signal.SIGTERM): 70 | loop.add_signal_handler( 71 | sig=sig, callback=lambda s=sig: handle_stop_signals(signum=s, frame=None) 72 | ) 73 | 74 | 75 | async def main(): 76 | try: 77 | os.makedirs(name=Config.DOWNLOAD_LOCATION, exist_ok=True) 78 | os.makedirs(name=Config.THUMB_LOCATION, exist_ok=True) 79 | 80 | if os.path.exists(Config.LOCKFILE): 81 | os.remove(path=Config.LOCKFILE) 82 | 83 | with open(file=Config.LOCKFILE, mode="w") as lock_f: 84 | lock_f.close() 85 | 86 | LOGGER.info(msg=messages.get(file="main", key="STARTING_BOT")) 87 | await unzipbot_client.start() 88 | starttime = time.strftime("%Y/%m/%d - %H:%M:%S") 89 | await unzipbot_client.send_message( 90 | chat_id=Config.LOGS_CHANNEL, 91 | text=messages.get(file="main", key="START_TXT", extra_args=starttime), 92 | ) 93 | await set_boot_time() 94 | LOGGER.info(msg=messages.get(file="main", key="CHECK_LOG")) 95 | 96 | if await check_logs(): 97 | LOGGER.info(msg=messages.get(file="main", key="LOG_CHECKED")) 98 | setup_signal_handlers() 99 | await remove_expired_tasks(True) 100 | await dl_thumbs() 101 | await start_cron_jobs() 102 | os.remove(path=Config.LOCKFILE) 103 | LOGGER.info(msg=messages.get(file="main", key="BOT_RUNNING")) 104 | await idle() 105 | else: 106 | try: 107 | await unzipbot_client.send_message( 108 | chat_id=Config.BOT_OWNER, 109 | text=messages.get( 110 | file="main", key="WRONG_LOG", extra_args=Config.LOGS_CHANNEL 111 | ), 112 | ) 113 | except: 114 | pass 115 | 116 | os.remove(path=Config.LOCKFILE) 117 | await async_shutdown_bot() 118 | except Exception as e: 119 | LOGGER.error(msg=messages.get(file="main", key="ERROR_MAIN_LOOP", extra_args=e)) 120 | finally: 121 | if os.path.exists(Config.LOCKFILE): 122 | os.remove(path=Config.LOCKFILE) 123 | await async_shutdown_bot() 124 | 125 | 126 | if __name__ == "__main__": 127 | unzipbot_client.run(main()) 128 | -------------------------------------------------------------------------------- /unzipbot/helpers/database.py: -------------------------------------------------------------------------------- 1 | from asyncio import sleep 2 | 3 | import base58check 4 | from motor.motor_asyncio import AsyncIOMotorClient 5 | from pyrogram.errors import FloodPremiumWait, FloodWait 6 | 7 | from config import Config 8 | from unzipbot import unzipbot_client 9 | from unzipbot.i18n.messages import Messages 10 | 11 | mongodb = AsyncIOMotorClient(host=Config.MONGODB_URL) 12 | unzip_db = mongodb[Config.MONGODB_DBNAME] 13 | 14 | 15 | def get_lang(user_id): 16 | return "en" 17 | 18 | 19 | messages = Messages(lang_fetcher=get_lang) 20 | 21 | # Users Database 22 | user_db = unzip_db["users_db"] 23 | 24 | 25 | async def add_user(user_id): 26 | new_user_id = int(user_id) 27 | is_exist = await user_db.find_one(filter={"user_id": new_user_id}) 28 | 29 | if is_exist is not None and is_exist: 30 | return -1 31 | 32 | await user_db.insert_one(document={"user_id": new_user_id}) 33 | 34 | 35 | async def del_user(user_id): 36 | del_user_id = int(user_id) 37 | is_exist = await user_db.find_one(filter={"user_id": del_user_id}) 38 | 39 | if is_exist is not None and is_exist: 40 | await user_db.delete_one(filter={"user_id": del_user_id}) 41 | 42 | else: 43 | return -1 44 | 45 | 46 | async def is_user_in_db(user_id): 47 | u_id = int(user_id) 48 | is_exist = await user_db.find_one(filter={"user_id": u_id}) 49 | 50 | if is_exist is not None and is_exist: 51 | return True 52 | 53 | return False 54 | 55 | 56 | async def count_users(): 57 | users = await user_db.count_documents(filter={}) 58 | 59 | return users 60 | 61 | 62 | async def get_users_list(): 63 | return [users_list async for users_list in user_db.find({})] 64 | 65 | 66 | # Banned users database 67 | b_user_db = unzip_db["banned_users_db"] 68 | 69 | 70 | async def add_banned_user(user_id): 71 | new_user_id = int(user_id) 72 | is_exist = await b_user_db.find_one(filter={"banned_user_id": new_user_id}) 73 | 74 | if is_exist is not None and is_exist: 75 | return -1 76 | 77 | await b_user_db.insert_one(document={"banned_user_id": new_user_id}) 78 | 79 | 80 | async def del_banned_user(user_id): 81 | del_user_id = int(user_id) 82 | is_exist = await b_user_db.find_one(filter={"banned_user_id": del_user_id}) 83 | 84 | if is_exist is not None and is_exist: 85 | await b_user_db.delete_one(filter={"banned_user_id": del_user_id}) 86 | else: 87 | return -1 88 | 89 | 90 | async def is_user_in_bdb(user_id): 91 | u_id = int(user_id) 92 | is_exist = await b_user_db.find_one(filter={"banned_user_id": u_id}) 93 | 94 | if is_exist is not None and is_exist: 95 | return True 96 | 97 | return False 98 | 99 | 100 | async def count_banned_users(): 101 | users = await b_user_db.count_documents(filter={}) 102 | 103 | return users 104 | 105 | 106 | async def get_banned_users_list(): 107 | return [banned_users_list async for banned_users_list in b_user_db.find({})] 108 | 109 | 110 | async def check_user(message): 111 | # Checking if user is banned 112 | uid = message.from_user.id 113 | is_banned = await is_user_in_bdb(uid) 114 | 115 | if is_banned: 116 | await message.reply(messages.get(file="database", key="BANNED")) 117 | await message.stop_propagation() 118 | 119 | return 120 | 121 | # Checking if user already in db 122 | is_in_db = await is_user_in_db(uid) 123 | 124 | if not is_in_db: 125 | await add_user(uid) 126 | 127 | try: 128 | firstname = message.from_user.first_name 129 | except: 130 | firstname = " " 131 | 132 | try: 133 | lastname = message.from_user.last_name 134 | except: 135 | lastname = " " 136 | 137 | try: 138 | username = message.from_user.username 139 | except: 140 | username = " " 141 | 142 | if firstname == " " and lastname == " " and username == " ": 143 | uname = message.from_user.mention 144 | 145 | try: 146 | await unzipbot_client.send_message( 147 | chat_id=Config.LOGS_CHANNEL, 148 | text=messages.get( 149 | file="database", 150 | key="NEW_USER_BAD", 151 | user_id=uid, 152 | extra_args=uname, 153 | ), 154 | disable_web_page_preview=False, 155 | ) 156 | except (FloodWait, FloodPremiumWait) as f: 157 | await sleep(f.value) 158 | await unzipbot_client.send_message( 159 | chat_id=Config.LOGS_CHANNEL, 160 | text=messages.get( 161 | file="database", 162 | key="NEW_USER_BAD", 163 | user_id=uid, 164 | extra_args=uname, 165 | ), 166 | disable_web_page_preview=False, 167 | ) 168 | else: 169 | if firstname is None: 170 | firstname = " " 171 | 172 | if lastname is None: 173 | lastname = " " 174 | 175 | if username is None: 176 | username = " " 177 | 178 | uname = firstname + " " + lastname 179 | umention = " | @" + username 180 | 181 | try: 182 | await unzipbot_client.send_message( 183 | chat_id=Config.LOGS_CHANNEL, 184 | text=messages.get( 185 | file="database", 186 | key="NEW_USER", 187 | user_id=uid, 188 | extra_args=[uname, umention, uid, uid, uid], 189 | ), 190 | disable_web_page_preview=False, 191 | ) 192 | except (FloodWait, FloodPremiumWait) as f: 193 | await sleep(f.value) 194 | await unzipbot_client.send_message( 195 | chat_id=Config.LOGS_CHANNEL, 196 | text=messages.get( 197 | file="database", 198 | key="NEW_USER", 199 | user_id=uid, 200 | extra_args=[uname, umention, uid, uid, uid], 201 | ), 202 | disable_web_page_preview=False, 203 | ) 204 | 205 | await message.continue_propagation() 206 | 207 | 208 | async def get_all_users(): 209 | users = [] 210 | banned = [] 211 | 212 | for i in range(await count_users()): 213 | users.append((await get_users_list())[i]["user_id"]) 214 | 215 | for j in range(await count_banned_users()): 216 | banned.append((await get_banned_users_list())[j]["banned_user_id"]) 217 | 218 | return users, banned 219 | 220 | 221 | # Upload mode 222 | mode_db = unzip_db["ulmode_db"] 223 | 224 | 225 | async def set_upload_mode(user_id, mode): 226 | is_exist = await mode_db.find_one(filter={"_id": user_id}) 227 | 228 | if is_exist is not None and is_exist: 229 | await mode_db.update_one( 230 | filter={"_id": user_id}, update={"$set": {"mode": mode}} 231 | ) 232 | else: 233 | await mode_db.insert_one(document={"_id": user_id, "mode": mode}) 234 | 235 | 236 | async def get_upload_mode(user_id): 237 | umode = await mode_db.find_one(filter={"_id": user_id}) 238 | 239 | if umode is not None and umode: 240 | return umode.get("mode") 241 | 242 | return "media" 243 | 244 | 245 | # Db for how many files user uploaded 246 | uploaded_db = unzip_db["uploaded_count_db"] 247 | 248 | 249 | async def get_uploaded(user_id): 250 | up_count = await uploaded_db.find_one(filter={"_id": user_id}) 251 | 252 | if up_count is not None and up_count: 253 | return up_count.get("uploaded_files") 254 | 255 | return 0 256 | 257 | 258 | async def update_uploaded(user_id, upload_count): 259 | is_exist = await uploaded_db.find_one(filter={"_id": user_id}) 260 | 261 | if is_exist is not None and is_exist: 262 | new_count = await get_uploaded(user_id) + upload_count 263 | await uploaded_db.update_one( 264 | filter={"_id": user_id}, update={"$set": {"uploaded_files": new_count}} 265 | ) 266 | else: 267 | await uploaded_db.insert_one( 268 | document={"_id": user_id, "uploaded_files": upload_count} 269 | ) 270 | 271 | 272 | # DB for thumbnails 273 | thumb_db = unzip_db["thumb_db"] 274 | 275 | 276 | async def get_thumb(user_id): 277 | existing = await thumb_db.find_one(filter={"_id": user_id}) 278 | 279 | if existing is not None and existing: 280 | return existing 281 | 282 | return None 283 | 284 | 285 | async def update_temp_thumb(user_id, thumb_id): 286 | existing = await thumb_db.find_one(filter={"_id": user_id}) 287 | 288 | if existing is not None and existing: 289 | await thumb_db.update_one( 290 | filter={"_id": user_id}, update={"$set": {"temp": thumb_id}} 291 | ) 292 | else: 293 | await thumb_db.insert_one(document={"_id": user_id, "temp": thumb_id}) 294 | 295 | 296 | async def update_thumb(user_id): 297 | existing = await thumb_db.find_one(filter={"_id": user_id}) 298 | 299 | if existing is not None and existing: 300 | await thumb_db.update_one( 301 | filter={"_id": user_id}, update={"$set": {"file_id": existing.get("temp")}} 302 | ) 303 | await thumb_db.update_one( 304 | filter={"_id": user_id}, update={"$unset": {"temp": ""}} 305 | ) 306 | 307 | if existing.get("url") is not None: 308 | await thumb_db.update_one( 309 | filter={"_id": user_id}, update={"$unset": {"url": ""}} 310 | ) 311 | else: 312 | return 313 | 314 | 315 | async def get_thumb_users(): 316 | thumb_users = [] 317 | 318 | async for thumb_list in thumb_db.find({}): 319 | if ( 320 | "file_id" in thumb_list 321 | and thumb_list["file_id"] is None 322 | and "url" not in thumb_list 323 | ) or ( 324 | "temp" in thumb_list 325 | and "file_id" not in thumb_list 326 | and "url" not in thumb_list 327 | ): 328 | await thumb_db.delete_one(filter={"_id": thumb_list["_id"]}) 329 | else: 330 | thumb_users.append(thumb_list) 331 | 332 | return thumb_users 333 | 334 | 335 | async def count_thumb_users(): 336 | users = await thumb_db.count_documents(filter={}) 337 | 338 | return users 339 | 340 | 341 | async def del_thumb_db(user_id): 342 | del_thumb_id = int(user_id) 343 | is_exist = await thumb_db.find_one(filter={"_id": del_thumb_id}) 344 | 345 | if is_exist is not None and is_exist: 346 | await thumb_db.delete_one(filter={"_id": del_thumb_id}) 347 | else: 348 | return 349 | 350 | 351 | # DB for bot data 352 | bot_data = unzip_db["bot_data"] 353 | 354 | 355 | async def get_boot(): 356 | boot = await bot_data.find_one(filter={"boot": True}) 357 | 358 | if boot is not None and boot: 359 | return boot.get("time") 360 | 361 | return None 362 | 363 | 364 | async def set_boot(boottime): 365 | is_exist = await bot_data.find_one(filter={"boot": True}) 366 | 367 | if is_exist is not None and is_exist: 368 | await bot_data.update_one( 369 | filter={"boot": True}, update={"$set": {"time": boottime}} 370 | ) 371 | else: 372 | await bot_data.insert_one(document={"boot": True, "time": boottime}) 373 | 374 | 375 | async def set_old_boot(boottime): 376 | is_exist = await bot_data.find_one(filter={"old_boot": True}) 377 | 378 | if is_exist is not None and is_exist: 379 | await bot_data.update_one( 380 | filter={"old_boot": True}, update={"$set": {"time": boottime}} 381 | ) 382 | else: 383 | await bot_data.insert_one(document={"old_boot": True, "time": boottime}) 384 | 385 | 386 | async def get_old_boot(): 387 | old_boot = await bot_data.find_one(filter={"old_boot": True}) 388 | 389 | if old_boot is not None and old_boot: 390 | return old_boot.get("time") 391 | 392 | return None 393 | 394 | 395 | async def is_boot_different(): 396 | different = True 397 | is_exist = await bot_data.find_one(filter={"boot": True}) 398 | is_exist_old = await bot_data.find_one(filter={"old_boot": True}) 399 | 400 | if is_exist and is_exist_old and is_exist.get("time") == is_exist_old.get("time"): 401 | different = False 402 | 403 | return different 404 | 405 | 406 | # DB for ongoing tasks 407 | ongoing_tasks = unzip_db["ongoing_tasks"] 408 | 409 | 410 | async def get_ongoing_tasks(): 411 | return [ongoing_list async for ongoing_list in ongoing_tasks.find({})] 412 | 413 | 414 | async def count_ongoing_tasks(): 415 | tasks = await ongoing_tasks.count_documents(filter={}) 416 | 417 | return tasks 418 | 419 | 420 | async def add_ongoing_task(user_id, start_time, task_type): 421 | await ongoing_tasks.insert_one( 422 | document={"user_id": user_id, "start_time": start_time, "type": task_type} 423 | ) 424 | 425 | 426 | async def del_ongoing_task(user_id): 427 | is_exist = await ongoing_tasks.find_one(filter={"user_id": user_id}) 428 | 429 | if is_exist is not None and is_exist: 430 | await ongoing_tasks.delete_one(filter={"user_id": user_id}) 431 | else: 432 | return 433 | 434 | 435 | async def clear_ongoing_tasks(): 436 | await ongoing_tasks.delete_many(filter={}) 437 | 438 | 439 | # DB for cancel tasks (that's stupid) 440 | cancel_tasks = unzip_db["cancel_tasks"] 441 | 442 | 443 | async def get_cancel_tasks(): 444 | return [cancel_list async for cancel_list in cancel_tasks.find({})] 445 | 446 | 447 | async def count_cancel_tasks(): 448 | tasks = await cancel_tasks.count_documents(filter={}) 449 | 450 | return tasks 451 | 452 | 453 | async def add_cancel_task(user_id): 454 | if not await get_cancel_task(user_id): 455 | await cancel_tasks.insert_one(document={"user_id": user_id}) 456 | 457 | 458 | async def del_cancel_task(user_id): 459 | is_exist = await cancel_tasks.find_one(filter={"user_id": user_id}) 460 | 461 | if is_exist is not None and is_exist: 462 | await cancel_tasks.delete_one(filter={"user_id": user_id}) 463 | else: 464 | return 465 | 466 | 467 | async def get_cancel_task(user_id): 468 | is_exist = await cancel_tasks.find_one(filter={"user_id": user_id}) 469 | 470 | return bool(is_exist is not None and is_exist) 471 | 472 | 473 | async def clear_cancel_tasks(): 474 | await cancel_tasks.delete_many(filter={}) 475 | 476 | 477 | # DB for merge tasks 478 | merge_tasks = unzip_db["merge_tasks"] 479 | 480 | 481 | async def get_merge_tasks(): 482 | return [merge_list async for merge_list in merge_tasks.find({})] 483 | 484 | 485 | async def count_merge_tasks(): 486 | tasks = await merge_tasks.count_documents(filter={}) 487 | 488 | return tasks 489 | 490 | 491 | async def add_merge_task(user_id, message_id): 492 | if not await get_merge_task(user_id): 493 | await merge_tasks.insert_one( 494 | document={"user_id": user_id, "message_id": message_id} 495 | ) 496 | else: 497 | await merge_tasks.update_one( 498 | filter={"user_id": user_id}, update={"$set": {"message_id": message_id}} 499 | ) 500 | 501 | 502 | async def del_merge_task(user_id): 503 | is_exist = await merge_tasks.find_one(filter={"user_id": user_id}) 504 | 505 | if is_exist is not None and is_exist: 506 | await merge_tasks.delete_one(filter={"user_id": user_id}) 507 | else: 508 | return 509 | 510 | 511 | async def get_merge_task(user_id): 512 | is_exist = await merge_tasks.find_one(filter={"user_id": user_id}) 513 | 514 | return bool(is_exist is not None and is_exist) 515 | 516 | 517 | async def get_merge_task_message_id(user_id): 518 | is_exist = await merge_tasks.find_one(filter={"user_id": user_id}) 519 | 520 | if is_exist is not None and is_exist: 521 | return is_exist.get("message_id") 522 | 523 | return False 524 | 525 | 526 | async def clear_merge_tasks(): 527 | await merge_tasks.delete_many(filter={}) 528 | 529 | 530 | # DB for maintenance mode 531 | maintenance_mode = unzip_db["maintenance_mode"] 532 | 533 | 534 | async def get_maintenance(): 535 | maintenance = await maintenance_mode.find_one(filter={"maintenance": True}) 536 | 537 | if maintenance is not None and maintenance: 538 | return maintenance.get("val") 539 | 540 | return False 541 | 542 | 543 | async def set_maintenance(val): 544 | is_exist = await maintenance_mode.find_one(filter={"maintenance": True}) 545 | 546 | if is_exist is not None and is_exist: 547 | await maintenance_mode.update_one( 548 | filter={"maintenance": True}, update={"$set": {"val": val}} 549 | ) 550 | else: 551 | await maintenance_mode.insert_one(document={"maintenance": True, "val": val}) 552 | 553 | 554 | # DB for VIP users 555 | vip_users = unzip_db["vip_users"] 556 | 557 | 558 | async def add_vip_user( 559 | uid, 560 | subscription, 561 | ends, 562 | used, 563 | billed, 564 | early, 565 | donator, 566 | started, 567 | successful, 568 | gap, 569 | gifted, 570 | referral, 571 | lifetime, 572 | ): 573 | is_exist = await vip_users.find_one(filter={"_id": uid}) 574 | 575 | if is_exist is not None and is_exist: 576 | await vip_users.update_one( 577 | filter={"_id": uid}, 578 | update={ 579 | "$set": { 580 | "subscription": subscription, 581 | "ends": ends, 582 | "used": used, 583 | "billed": billed, 584 | "early": early, 585 | "donator": donator, 586 | "started": started, 587 | "successful": successful, 588 | "gap": gap, 589 | "gifted": gifted, 590 | "referral": referral, 591 | "lifetime": lifetime, 592 | } 593 | }, 594 | ) 595 | else: 596 | await vip_users.insert_one( 597 | document={ 598 | "_id": uid, 599 | "subscription": subscription, 600 | "ends": ends, 601 | "used": used, 602 | "billed": billed, 603 | "early": early, 604 | "donator": donator, 605 | "started": started, 606 | "successful": successful, 607 | "gap": gap, 608 | "gifted": gifted, 609 | "referral": referral, 610 | "lifetime": lifetime, 611 | } 612 | ) 613 | 614 | 615 | async def remove_vip_user(uid): 616 | is_exist = await vip_users.find_one(filter={"_id": uid}) 617 | 618 | if is_exist is not None and is_exist: 619 | await vip_users.delete_one(filter={"_id": uid}) 620 | else: 621 | return 622 | 623 | 624 | async def is_vip(uid): 625 | is_exist = await vip_users.find_one(filter={"_id": uid}) 626 | 627 | return bool(is_exist is not None and is_exist) 628 | 629 | 630 | async def get_vip_users(): 631 | return [vip_list async for vip_list in vip_users.find({})] 632 | 633 | 634 | async def count_vip_users(): 635 | users = await vip_users.count_documents(filter={}) 636 | 637 | return users 638 | 639 | 640 | async def get_vip_user(uid): 641 | is_exist = await vip_users.find_one(filter={"_id": uid}) 642 | 643 | if is_exist is not None and is_exist: 644 | return is_exist 645 | 646 | return None 647 | 648 | 649 | # DB for referrals 650 | referrals = unzip_db["referrals"] 651 | 652 | 653 | async def add_referee(uid, referral_code): 654 | is_exist = await referrals.find_one(filter={"_id": uid}) 655 | 656 | if is_exist is not None and is_exist: 657 | await referrals.update_one( 658 | filter={"_id": uid}, 659 | update={"$set": {"type": "referee", "referral_code": referral_code}}, 660 | ) 661 | else: 662 | await referrals.insert_one( 663 | document={"_id": uid, "type": "referee", "referral_code": referral_code} 664 | ) 665 | 666 | 667 | async def add_referrer(uid, referees): 668 | is_exist = await referrals.find_one(filter={"_id": uid}) 669 | 670 | if is_exist is not None and is_exist: 671 | await referrals.update_one( 672 | filter={"_id": uid}, 673 | update={"$set": {"type": "referrer", "referees": referees}}, 674 | ) 675 | else: 676 | await referrals.insert_one( 677 | document={"_id": uid, "type": "referrer", "referees": referees} 678 | ) 679 | 680 | 681 | async def get_referee(uid): 682 | is_exist = await referrals.find_one(filter={"_id": uid}) 683 | 684 | if is_exist is not None and is_exist: 685 | return is_exist 686 | 687 | return None 688 | 689 | 690 | async def get_referrer(uid): 691 | is_exist = await referrals.find_one(filter={"_id": uid}) 692 | 693 | if is_exist is not None and is_exist: 694 | return is_exist 695 | 696 | return None 697 | 698 | 699 | def get_referral_code(uid): 700 | return base58check.b58encode( 701 | val=base58check.b58encode(val=str(uid).encode(encoding="ascii")) 702 | ).decode(encoding="ascii") 703 | 704 | 705 | def get_referral_uid(referral_code): 706 | return int( 707 | base58check.b58decode( 708 | val=base58check.b58decode(val=referral_code).decode(encoding="ascii") 709 | ).decode(encoding="ascii") 710 | ) 711 | -------------------------------------------------------------------------------- /unzipbot/helpers/start.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import shutil 4 | from datetime import datetime 5 | from time import time 6 | 7 | import aiocron 8 | from pyrogram import enums 9 | from pyrogram.errors import FloodPremiumWait, FloodWait 10 | 11 | from config import Config 12 | from unzipbot import LOGGER, boottime, unzipbot_client 13 | from unzipbot.i18n.messages import Messages 14 | from unzipbot.modules.callbacks import download 15 | 16 | from .database import ( 17 | clear_cancel_tasks, 18 | clear_merge_tasks, 19 | clear_ongoing_tasks, 20 | count_ongoing_tasks, 21 | del_ongoing_task, 22 | get_boot, 23 | get_lang, 24 | get_old_boot, 25 | get_ongoing_tasks, 26 | get_thumb_users, 27 | is_boot_different, 28 | set_boot, 29 | set_old_boot, 30 | ) 31 | 32 | 33 | def get_size(doc_f): 34 | try: 35 | fsize = os.stat(path=doc_f).st_size 36 | return fsize 37 | except: 38 | return -1 39 | 40 | 41 | messages = Messages(lang_fetcher=get_lang) 42 | 43 | 44 | async def check_logs(): 45 | try: 46 | if Config.LOGS_CHANNEL: 47 | c_info = await unzipbot_client.get_chat(chat_id=Config.LOGS_CHANNEL) 48 | 49 | if c_info.type in (enums.ChatType.PRIVATE, enums.ChatType.BOT): 50 | LOGGER.error(msg=messages.get(file="start", key="PRIVATE_CHAT")) 51 | 52 | return False 53 | 54 | return True 55 | 56 | LOGGER.error(msg=messages.get(file="start", key="NO_LOG_ID")) 57 | 58 | return False 59 | except: 60 | LOGGER.error(msg=messages.get(file="start", key="ERROR_LOG_CHECK")) 61 | 62 | return False 63 | 64 | 65 | async def dl_thumbs(): 66 | thumbs = await get_thumb_users() 67 | i = 0 68 | maxthumbs = len(thumbs) 69 | LOGGER.info(msg=messages.get(file="start", key="DL_THUMBS", extra_args=maxthumbs)) 70 | 71 | for thumb in thumbs: 72 | file_path = Config.THUMB_LOCATION + "/" + str(thumb.get("_id")) + ".jpg" 73 | 74 | if not os.path.exists(file_path): 75 | if thumb.get("url") is None and thumb.get("file_id") is not None: 76 | try: 77 | await unzipbot_client.download_media( 78 | message=thumb.get("file_id"), file_name=file_path 79 | ) 80 | except: 81 | # Here we could encounter 400 FILE_REFERENCE_EXPIRED 82 | # A possible fix is to retrieve the message again with chat ID 83 | # + message ID to get a refreshed file reference 84 | await unzipbot_client.send_message( 85 | chat_id=thumb.get("_id"), 86 | text=messages.get( 87 | file="start", key="MISSING_THUMB", user_id=thumb.get("_id") 88 | ), 89 | ) 90 | elif thumb.get("url") is not None and thumb.get("file_id") is None: 91 | await download(url=thumb.get("url"), path=file_path) 92 | 93 | if get_size(file_path) in (0, -1): 94 | os.remove(path=file_path) 95 | 96 | i += 1 97 | 98 | if i % 10 == 0 or i == maxthumbs: 99 | LOGGER.info( 100 | msg=messages.get( 101 | file="start", key="DOWNLOADED_THUMBS", extra_args=[i, maxthumbs] 102 | ) 103 | ) 104 | 105 | 106 | async def set_boot_time(): 107 | boot = await get_boot() 108 | await set_old_boot(boot) 109 | await set_boot(boottime) 110 | boot = await get_boot() 111 | old_boot = await get_old_boot() 112 | different = await is_boot_different() 113 | 114 | if different: 115 | try: 116 | await unzipbot_client.send_message( 117 | chat_id=Config.BOT_OWNER, 118 | text=messages.get( 119 | file="start", 120 | key="BOT_RESTARTED", 121 | user_id=Config.BOT_OWNER, 122 | extra_args=[ 123 | datetime.fromtimestamp(timestamp=old_boot).strftime( 124 | r"%d/%m/%Y - %H:%M:%S" 125 | ), 126 | datetime.fromtimestamp(timestamp=boot).strftime( 127 | r"%d/%m/%Y - %H:%M:%S" 128 | ), 129 | ], 130 | ), 131 | ) 132 | except: 133 | pass # first start obviously 134 | 135 | await warn_users() 136 | 137 | 138 | async def warn_users(): 139 | await clear_cancel_tasks() 140 | await clear_merge_tasks() 141 | 142 | if await count_ongoing_tasks() > 0: 143 | tasks = await get_ongoing_tasks() 144 | 145 | for task in tasks: 146 | try: 147 | await unzipbot_client.send_message( 148 | chat_id=task.get("user_id"), 149 | text=messages.get( 150 | file="start", key="RESEND_TASK", user_id=task.get("user_id") 151 | ), 152 | ) 153 | except (FloodWait, FloodPremiumWait) as f: 154 | await asyncio.sleep(f.value) 155 | await unzipbot_client.send_message( 156 | chat_id=task.get("user_id"), 157 | text=messages.get( 158 | file="start", key="RESEND_TASK", user_id=task.get("user_id") 159 | ), 160 | ) 161 | except: 162 | pass # user deleted chat 163 | 164 | await clear_ongoing_tasks() 165 | 166 | 167 | async def remove_expired_tasks(firststart=False): 168 | ongoing_tasks = await get_ongoing_tasks() 169 | await clear_cancel_tasks() 170 | 171 | if firststart: 172 | await clear_ongoing_tasks() 173 | 174 | try: 175 | shutil.rmtree(Config.DOWNLOAD_LOCATION) 176 | except: 177 | pass 178 | 179 | os.makedirs(name=Config.DOWNLOAD_LOCATION, exist_ok=True) 180 | else: 181 | for task in ongoing_tasks: 182 | user_id = task.get("user_id") 183 | 184 | if user_id != Config.BOT_OWNER: 185 | current_time = time() 186 | start_time = task.get("start_time") 187 | task_type = task.get("type") 188 | time_gap = current_time - start_time 189 | 190 | if task_type == "extract": 191 | if time_gap > Config.MAX_TASK_DURATION_EXTRACT: 192 | try: 193 | await del_ongoing_task(user_id) 194 | shutil.rmtree(f"{Config.DOWNLOAD_LOCATION}/{user_id}") 195 | except: 196 | pass 197 | await unzipbot_client.send_message( 198 | chat_id=user_id, 199 | text=messages.get( 200 | file="start", 201 | key="TASK_EXPIRED", 202 | user_id=user_id, 203 | extra_args=Config.MAX_TASK_DURATION_EXTRACT // 60, 204 | ), 205 | ) 206 | elif task_type == "merge": 207 | if time_gap > Config.MAX_TASK_DURATION_MERGE: 208 | try: 209 | await del_ongoing_task(user_id) 210 | shutil.rmtree(f"{Config.DOWNLOAD_LOCATION}/{user_id}") 211 | except: 212 | pass 213 | await unzipbot_client.send_message( 214 | chat_id=user_id, 215 | text=messages.get( 216 | file="start", 217 | key="TASK_EXPIRED", 218 | user_id=user_id, 219 | extra_args=Config.MAX_TASK_DURATION_MERGE // 60, 220 | ), 221 | ) 222 | 223 | 224 | @aiocron.crontab("*/5 * * * *") 225 | async def scheduled_remove_expired_tasks(): 226 | await remove_expired_tasks() 227 | 228 | 229 | async def start_cron_jobs(): 230 | scheduled_remove_expired_tasks.start() 231 | -------------------------------------------------------------------------------- /unzipbot/helpers/unzip_help.py: -------------------------------------------------------------------------------- 1 | import math 2 | import time 3 | from asyncio import sleep 4 | 5 | import psutil 6 | from pyrogram import enums 7 | from pyrogram.errors import FloodPremiumWait, FloodWait 8 | 9 | from config import Config 10 | from unzipbot.helpers.database import del_cancel_task, get_cancel_task, get_lang 11 | from unzipbot.i18n.buttons import Buttons 12 | from unzipbot.i18n.messages import Messages 13 | 14 | messages = Messages(lang_fetcher=get_lang) 15 | 16 | 17 | async def progress_for_pyrogram(current, total, ud_type, message, start, unzip_bot): 18 | if not message: 19 | return 20 | 21 | uid = message.chat.id 22 | 23 | if message.chat.type == enums.ChatType.PRIVATE and await get_cancel_task(uid): 24 | await del_cancel_task(uid) 25 | await message.edit( 26 | text=messages.get(file="unzip_help", key="DL_STOPPED", user_id=uid) 27 | ) 28 | unzip_bot.stop_transmission() 29 | else: 30 | now = time.time() 31 | diff = now - start 32 | 33 | if total == 0: 34 | tmp = messages.get(file="unzip_help", key="UNKNOWN_SIZE", user_id=uid) 35 | 36 | try: 37 | await message.edit( 38 | text=messages.get( 39 | file="unzip_help", 40 | key="PROGRESS_MSG", 41 | user_id=uid, 42 | extra_args=[ud_type, tmp], 43 | ), 44 | reply_markup=Buttons.I_PREFER_STOP, 45 | ) 46 | except (FloodWait, FloodPremiumWait) as f: 47 | await sleep(f.value) 48 | await message.edit( 49 | text=messages.get( 50 | file="unzip_help", 51 | key="PROGRESS_MSG", 52 | user_id=uid, 53 | extra_args=[ud_type, tmp], 54 | ), 55 | reply_markup=Buttons.I_PREFER_STOP, 56 | ) 57 | except: 58 | pass 59 | elif round(number=diff % 10.00) == 0 or current == total: 60 | percentage = current * 100 / total 61 | speed = current / diff 62 | estimated_total_time = round(number=(total - current) / speed) * 1000 63 | estimated_total_time = TimeFormatter(milliseconds=estimated_total_time) 64 | filled = "".join(["⬢" for _ in range(math.floor(percentage / 5))]) 65 | empty = "".join(["⬡" for _ in range(20 - math.floor(percentage / 5))]) 66 | progress = f"[{filled}{empty}] \n" 67 | progress += ( 68 | f"{messages.get(file='unzip_help', key='PROCESSING', user_id=uid)} : " 69 | f"`{round(number=percentage, ndigits=2)}%`\n" 70 | ) 71 | eta = ( 72 | estimated_total_time 73 | if estimated_total_time != "" or percentage != "100" 74 | else "0 s" 75 | ) 76 | tmp = ( 77 | progress 78 | + f"`{humanbytes(current)} of {humanbytes(total)}`\n" 79 | + f"{messages.get(file='unzip_help', key='SPEED', user_id=uid)} " 80 | + f"`{humanbytes(speed)}/s`\n" 81 | + f"{messages.get(file='unzip_help', key='ETA', user_id=uid)} " 82 | + f"`{eta}`\n" 83 | ) 84 | 85 | try: 86 | await message.edit( 87 | text=messages.get( 88 | file="unzip_help", 89 | key="PROGRESS_MSG", 90 | user_id=uid, 91 | extra_args=[ud_type, tmp], 92 | ), 93 | reply_markup=Buttons.I_PREFER_STOP, 94 | ) 95 | except (FloodWait, FloodPremiumWait) as f: 96 | await sleep(f.value) 97 | await message.edit( 98 | text=messages.get( 99 | file="unzip_help", 100 | key="PROGRESS_MSG", 101 | user_id=uid, 102 | extra_args=[ud_type, tmp], 103 | ), 104 | reply_markup=Buttons.I_PREFER_STOP, 105 | ) 106 | except: 107 | pass 108 | 109 | 110 | async def progress_urls(current, total, ud_type, message, start): 111 | now = time.time() 112 | diff = now - start 113 | uid = message.chat.id 114 | 115 | if round(number=diff % 10.00) == 0 or current == total: 116 | percentage = current * 100 / total 117 | speed = current / diff 118 | estimated_total_time = round(number=(total - current) / speed) * 1000 119 | estimated_total_time = TimeFormatter(milliseconds=estimated_total_time) 120 | filled = "".join(["⬢" for _ in range(math.floor(percentage / 5))]) 121 | empty = "".join(["⬡" for _ in range(20 - math.floor(percentage / 5))]) 122 | progress = f"[{filled}{empty}] \n" 123 | progress += ( 124 | f"{messages.get(file='unzip_help', key='PROCESSING', user_id=uid)} : " 125 | f"`{round(number=percentage, ndigits=2)}%`\n" 126 | ) 127 | eta = ( 128 | estimated_total_time 129 | if estimated_total_time != "" or percentage != "100" 130 | else "0 s" 131 | ) 132 | tmp = ( 133 | progress 134 | + f"`{humanbytes(current)} of {humanbytes(total)}`\n" 135 | + f"{messages.get(file='unzip_help', key='SPEED', user_id=uid)} " 136 | + f"`{humanbytes(speed)}/s`\n" 137 | + f"{messages.get(file='unzip_help', key='ETA', user_id=uid)} " 138 | + f"`{eta}`\n" 139 | ) 140 | 141 | try: 142 | await message.edit( 143 | messages.get( 144 | file="unzip_help", 145 | key="PROGRESS_MSG", 146 | user_id=uid, 147 | extra_args=[ud_type, tmp], 148 | ) 149 | ) 150 | except (FloodWait, FloodPremiumWait) as f: 151 | await sleep(f.value) 152 | await message.edit( 153 | messages.get( 154 | file="unzip_help", 155 | key="PROGRESS_MSG", 156 | user_id=uid, 157 | extra_args=[ud_type, tmp], 158 | ) 159 | ) 160 | except: 161 | pass 162 | 163 | 164 | def humanbytes(size): 165 | if not size: 166 | return "" 167 | 168 | power = 2**10 169 | n = 0 170 | Dic_powerN = {0: " ", 1: "Ki", 2: "Mi", 3: "Gi", 4: "Ti"} 171 | 172 | while size > power: 173 | size /= power 174 | n += 1 175 | 176 | return str(round(number=size, ndigits=2)) + " " + Dic_powerN.get(n) + "B" 177 | 178 | 179 | def TimeFormatter(milliseconds: int) -> str: 180 | seconds, milliseconds = divmod(int(milliseconds), 1000) 181 | minutes, seconds = divmod(seconds, 60) 182 | hours, minutes = divmod(minutes, 60) 183 | days, hours = divmod(hours, 24) 184 | tmp = ( 185 | ((str(days) + "d, ") if days else "") 186 | + ((str(hours) + "h, ") if hours else "") 187 | + ((str(minutes) + "m, ") if minutes else "") 188 | + ((str(seconds) + "s, ") if seconds else "") 189 | + ((str(milliseconds) + "ms, ") if milliseconds else "") 190 | ) 191 | 192 | return tmp[:-2] 193 | 194 | 195 | def timeformat_sec(seconds: int) -> str: 196 | minutes, seconds = divmod(int(seconds), 60) 197 | hours, minutes = divmod(minutes, 60) 198 | days, hours = divmod(hours, 24) 199 | tmp = ( 200 | ((str(days) + "d, ") if days else "") 201 | + ((str(hours) + "h, ") if hours else "") 202 | + ((str(minutes) + "m, ") if minutes else "") 203 | + ((str(seconds) + "s, ") if seconds else "") 204 | ) 205 | 206 | return tmp[:-2] 207 | 208 | 209 | def calculate_memory_limit(): 210 | if Config.MAX_RAM_AMOUNT_KB != -1: 211 | return int(Config.MAX_RAM_AMOUNT_KB * Config.MAX_RAM_USAGE / 100) 212 | 213 | # we may need to use virtual_memory().available instead of total 214 | total_memory = psutil.virtual_memory().total 215 | memory_limit_kb = int(total_memory * Config.MAX_RAM_USAGE / 100 / 1024) 216 | 217 | return memory_limit_kb 218 | 219 | 220 | # List of error messages from 7zip 221 | ERROR_MSGS = ["Error", "Can't open as archive"] 222 | 223 | # List of common extentions 224 | extentions_list = { 225 | "archive": [ 226 | "7z", 227 | "apk", 228 | "apkm", 229 | "apks", 230 | "appx", 231 | "arc", 232 | "bcm", 233 | "bin", 234 | "br", 235 | "bz2", 236 | "dmg", 237 | "exe", 238 | "gz", 239 | "img", 240 | "ipsw", 241 | "iso", 242 | "jar", 243 | "lz4", 244 | "msi", 245 | "paf", 246 | "pak", 247 | "pea", 248 | "pkg", 249 | "rar", 250 | "tar", 251 | "tgz", 252 | "wim", 253 | "x7", 254 | "xapk", 255 | "xz", 256 | "z", 257 | "zip", 258 | "zipx", 259 | "zpaq", 260 | "zst", 261 | "zstd", 262 | ], 263 | "audio": ["aac", "aif", "aiff", "alac", "flac", "m4a", "mp3", "ogg", "wav", "wma"], 264 | "photo": ["gif", "jpg", "jpeg", "png", "tiff", "webp"], 265 | "split": ["0*", "001", "002", "003", "004", "005", "006", "007", "008", "009"], 266 | "video": ["3gp", "avi", "flv", "mp4", "mkv", "mov", "mpeg", "mpg", "webm"], 267 | } 268 | 269 | tarball_extensions = ( 270 | ".tar.gz", 271 | ".gz", 272 | ".tgz", 273 | ".taz", 274 | ".tar.bz2", 275 | ".bz2", 276 | ".tb2", 277 | ".tbz", 278 | ".tbz2", 279 | ".tz2", 280 | ".tar.lz", 281 | ".lz", 282 | ".tar.lzma", 283 | ".lzma", 284 | ".tlz", 285 | ".tar.lzo", 286 | ".lzo", 287 | ".tar.xz", 288 | ".xz", 289 | ".txz", 290 | ".tar.z", 291 | ".z", 292 | ".tz", 293 | ".taz", 294 | ) 295 | -------------------------------------------------------------------------------- /unzipbot/i18n/buttons.py: -------------------------------------------------------------------------------- 1 | from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup 2 | 3 | from unzipbot.helpers.database import get_lang 4 | 5 | from .messages import Messages 6 | 7 | messages = Messages(lang_fetcher=get_lang) 8 | 9 | 10 | # Inline buttons 11 | class Buttons: 12 | START_BUTTON = InlineKeyboardMarkup( 13 | [ 14 | [ 15 | InlineKeyboardButton( 16 | text=messages.get(file="buttons", key="HELP"), 17 | callback_data="helpcallback", 18 | ), 19 | InlineKeyboardButton( 20 | text=messages.get(file="buttons", key="ABOUT"), 21 | callback_data="aboutcallback", 22 | ), 23 | ], 24 | [ 25 | InlineKeyboardButton( 26 | text=messages.get(file="buttons", key="STATS_BTN"), 27 | callback_data="statscallback", 28 | ), 29 | InlineKeyboardButton( 30 | text=messages.get(file="buttons", key="DONATE"), 31 | callback_data="donatecallback", 32 | ), 33 | ], 34 | ] 35 | ) 36 | 37 | REFRESH_BUTTON = InlineKeyboardMarkup( 38 | [ 39 | [ 40 | InlineKeyboardButton( 41 | text=messages.get(file="buttons", key="REFRESH"), 42 | callback_data="statscallback|refresh", 43 | ), 44 | InlineKeyboardButton( 45 | text=messages.get(file="buttons", key="BACK"), 46 | callback_data="megoinhome", 47 | ), 48 | ] 49 | ] 50 | ) 51 | 52 | CHOOSE_E_F__BTNS = InlineKeyboardMarkup( 53 | [ 54 | [ 55 | InlineKeyboardButton( 56 | text="🗂️", callback_data="extract_file|tg_file|no_pass" 57 | ), 58 | InlineKeyboardButton( 59 | text="🔐", callback_data="extract_file|tg_file|with_pass" 60 | ), 61 | ], 62 | [ 63 | InlineKeyboardButton( 64 | text="🖼️", callback_data="extract_file|tg_file|thumb" 65 | ), 66 | InlineKeyboardButton( 67 | text="✏", callback_data="extract_file|tg_file|thumbrename" 68 | ), 69 | ], 70 | [InlineKeyboardButton(text="❌", callback_data="cancel_dis")], 71 | ] 72 | ) 73 | 74 | CHOOSE_E_F_M__BTNS = InlineKeyboardMarkup( 75 | [ 76 | [ 77 | InlineKeyboardButton(text="🗂️", callback_data="merged|no_pass"), 78 | InlineKeyboardButton(text="🔐", callback_data="merged|with_pass"), 79 | ], 80 | [InlineKeyboardButton(text="❌", callback_data="cancel_dis")], 81 | ] 82 | ) 83 | 84 | CHOOSE_E_U__BTNS = InlineKeyboardMarkup( 85 | [ 86 | [ 87 | InlineKeyboardButton( 88 | text="🔗", callback_data="extract_file|url|no_pass" 89 | ), 90 | InlineKeyboardButton( 91 | text="🔐", callback_data="extract_file|url|with_pass" 92 | ), 93 | ], 94 | [ 95 | InlineKeyboardButton(text="🖼️", callback_data="extract_file|url|thumb"), 96 | InlineKeyboardButton( 97 | text="✏", callback_data="extract_file|url|thumbrename" 98 | ), 99 | ], 100 | [InlineKeyboardButton(text="❌", callback_data="cancel_dis")], 101 | ] 102 | ) 103 | 104 | RENAME = InlineKeyboardMarkup( 105 | [ 106 | [ 107 | InlineKeyboardButton(text="✏", callback_data="renameit"), 108 | InlineKeyboardButton(text="🙅‍♂️", callback_data="norename"), 109 | ] 110 | ] 111 | ) 112 | 113 | CLN_BTNS = InlineKeyboardMarkup( 114 | [ 115 | [ 116 | InlineKeyboardButton( 117 | text=messages.get(file="buttons", key="CLEAN"), 118 | callback_data="cancel_dis", 119 | ), 120 | InlineKeyboardButton( 121 | text=messages.get(file="buttons", key="CANCEL_IT"), 122 | callback_data="nobully", 123 | ), 124 | ] 125 | ] 126 | ) 127 | 128 | ME_GOIN_HOME = InlineKeyboardMarkup( 129 | [ 130 | [ 131 | InlineKeyboardButton( 132 | text=messages.get(file="buttons", key="BACK"), 133 | callback_data="megoinhome", 134 | ) 135 | ] 136 | ] 137 | ) 138 | 139 | SET_UPLOAD_MODE_BUTTONS = InlineKeyboardMarkup( 140 | [ 141 | [ 142 | InlineKeyboardButton( 143 | text=messages.get(file="buttons", key="AS_DOC"), 144 | callback_data="set_mode|doc", 145 | ), 146 | InlineKeyboardButton( 147 | text=messages.get(file="buttons", key="AS_MEDIA"), 148 | callback_data="set_mode|media", 149 | ), 150 | ] 151 | ] 152 | ) 153 | 154 | I_PREFER_STOP = InlineKeyboardMarkup( 155 | [ 156 | [ 157 | InlineKeyboardButton( 158 | text=messages.get(file="buttons", key="CANCEL_IT"), 159 | callback_data="canceldownload", 160 | ) 161 | ] 162 | ] 163 | ) 164 | 165 | MERGE_THEM_ALL = InlineKeyboardMarkup( 166 | [ 167 | [ 168 | InlineKeyboardButton( 169 | text=messages.get(file="buttons", key="MERGE_BTN"), 170 | callback_data="merge_this", 171 | ), 172 | InlineKeyboardButton( 173 | text=messages.get(file="buttons", key="CANCEL_IT"), 174 | callback_data="cancel_dis", 175 | ), 176 | ] 177 | ] 178 | ) 179 | 180 | THUMB_REPLACEMENT = InlineKeyboardMarkup( 181 | [ 182 | [ 183 | InlineKeyboardButton( 184 | text=messages.get(file="buttons", key="CHECK"), 185 | callback_data="check_thumb", 186 | ), 187 | InlineKeyboardButton( 188 | text=messages.get(file="buttons", key="REPLACE"), 189 | callback_data="save_thumb|replace", 190 | ), 191 | ], 192 | [ 193 | InlineKeyboardButton( 194 | text=messages.get(file="buttons", key="CANCEL_IT"), 195 | callback_data="nope_thumb", 196 | ) 197 | ], 198 | ] 199 | ) 200 | 201 | THUMB_FINAL = InlineKeyboardMarkup( 202 | [ 203 | [ 204 | InlineKeyboardButton( 205 | text=messages.get(file="buttons", key="REPLACE"), 206 | callback_data="save_thumb|replace", 207 | ), 208 | InlineKeyboardButton( 209 | text=messages.get(file="buttons", key="CANCEL_IT"), 210 | callback_data="nope_thumb", 211 | ), 212 | ] 213 | ] 214 | ) 215 | 216 | THUMB_SAVE = InlineKeyboardMarkup( 217 | [ 218 | [ 219 | InlineKeyboardButton( 220 | text=messages.get(file="buttons", key="SAVE"), 221 | callback_data="save_thumb|save", 222 | ), 223 | InlineKeyboardButton( 224 | text=messages.get(file="buttons", key="CANCEL_IT"), 225 | callback_data="nope_thumb", 226 | ), 227 | ] 228 | ] 229 | ) 230 | 231 | THUMB_DEL = InlineKeyboardMarkup( 232 | [ 233 | [ 234 | InlineKeyboardButton( 235 | text=messages.get(file="buttons", key="CHECK"), 236 | callback_data="check_before_del", 237 | ), 238 | InlineKeyboardButton( 239 | text=messages.get(file="buttons", key="DELETE"), 240 | callback_data="del_thumb", 241 | ), 242 | ], 243 | [ 244 | InlineKeyboardButton( 245 | text=messages.get(file="buttons", key="CANCEL_IT"), 246 | callback_data="nope_thumb", 247 | ) 248 | ], 249 | ] 250 | ) 251 | 252 | THUMB_DEL_2 = InlineKeyboardMarkup( 253 | [ 254 | [ 255 | InlineKeyboardButton( 256 | text=messages.get(file="buttons", key="DELETE"), 257 | callback_data="del_thumb", 258 | ), 259 | InlineKeyboardButton( 260 | text=messages.get(file="buttons", key="CANCEL_IT"), 261 | callback_data="nope_thumb", 262 | ), 263 | ] 264 | ] 265 | ) 266 | 267 | RATE_ME = InlineKeyboardMarkup( 268 | [ 269 | [ 270 | InlineKeyboardButton( 271 | text=messages.get(file="buttons", key="RATE"), 272 | url="https://t.me/BotsArchive/2705", 273 | ), 274 | InlineKeyboardButton( 275 | text=messages.get(file="buttons", key="DONATE"), 276 | callback_data="donatecallback", 277 | ), 278 | ] 279 | ] 280 | ) 281 | -------------------------------------------------------------------------------- /unzipbot/i18n/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttons": { 3 | "about": "About 👀", 4 | "as_doc": "As document 📁", 5 | "as_media": "As media 📺", 6 | "back": "Back 🏡", 7 | "cancel_it": "❌ Cancel", 8 | "check": "Check 👀", 9 | "clean": "Clean my files 🚮", 10 | "delete": "Delete 🚮", 11 | "donate": "Donate 💸", 12 | "help": "Help 📜", 13 | "merge_btn": "Merge 🛠️", 14 | "rate": "Rate me ⭐", 15 | "refresh": "Refresh ♻️", 16 | "replace": "Replace ⏭", 17 | "save": "Save 💾", 18 | "stats_btn": "Stats 📊" 19 | }, 20 | "callbacks": { 21 | "about_txt": "**About the unzip-bot [v{}]**\n\n• **Language :** [Python 3.12.9](https://www.python.org/)\n• **Framework :** [Mayuri-Chan/pyrofork 2.3.59](https://pyrofork.wulan17.top/)\n• **Source code :** [EDM115/unzip-bot@v7](https://github.com/EDM115/unzip-bot)\n• **Developer :** [EDM115](https://github.com/EDM115)\n\n**[Rate me ⭐](https://t.me/BotsArchive/2705)**\nMade with ❤️ by **@EDM115bots**", 22 | "actual_thumb": "Your actual thumbnail", 23 | "after_ok_dl_txt": "**Successfully downloaded ✅**\n\n**Download time :** `{}`\n**Status :** Testing the archive… Please wait", 24 | "after_ok_merge_dl_txt": "**Successfully downloaded all {} files ✅**\n\n**Download time :** `{}`\n**Status :** Merging the archive… Please wait", 25 | "after_ok_merge_txt": "**Successfully merged ✅**\n\n**Merge time :** `{}`\n**Status :** Processing the archive… Please wait", 26 | "after_ok_test_txt": "**Successfully tested ✅**\n\n**Test time :** `{}`\n**Status :** Extracting the archive… Please wait", 27 | "cancelled": "**Cancelled successfully ✅**", 28 | "cancelled_txt": "**{} ✅**", 29 | "cant_dl_url": "**Sorry, I can't download that URL 😭**", 30 | "changed_upload_mode_txt": "**Successfully changed the upload mode to** `{}` ✅", 31 | "choose_ext_mode_merge": "Select the extraction mode for that merged file 👀\n\n🗂️ : **Normal mode**\n🔐 : **Password protected**\n❌ : **Cancel your task**", 32 | "def_not_an_archive": "This file is NOT an archive 😐\nIf you believe it's an error, send the file to **@EDM115**", 33 | "del_confirm_thumb_2": "Do you really want to delete your thumbnail ?", 34 | "deleted_thumb": "**Successfully deleted your thumbnail ✅**", 35 | "dl_files": "**Downloading file {}/{}… Please wait**\n\n", 36 | "dl_stopped": "The download of your file has successfully been cancelled ✅", 37 | "dl_url": "**Downloading… Please wait**\n\n**URL :** `{}`\n", 38 | "donate_text": "I'm going to be honest : **this bot costs me money**…\nNothing's free in this world, however I try to keep this bot for free for as many people as possible\nI don't like to put restrictions, nor getting your PMs flooded with ads…\n\nSo if you can, donate :)\nIt helps out a ton, covers the costs (hosting, updating, … 👨‍💻)\n\n--How ?--\n• **[Paypal](https://www.paypal.me/8EDM115)**\n• **[GitHub Sponsors](https://github.com/sponsors/EDM115)**\n• **[Directly in Telegram](https://t.me/EDM115bots/698)**\n• **[BuyMeACoffee](https://www.buymeacoffee.com/edm115)**\n\nThanks for your contribution 😊\n\n--Side note :--\nDonation doesn't count as a VIP subscription. Check **/vip** for more info", 39 | "err_dl": "Error on download : {}", 40 | "err_split": "An error occured while splitting a file above 2 Gb 😥", 41 | "error_get_msg": "Error on getting messages from user : {}", 42 | "error_thumb_del": "Error on thumb deletion in DB : {}", 43 | "error_thumb_rename": "Error on thumb rename", 44 | "error_thumb_update": "Error while updating thumb URL on DB", 45 | "error_txt": "**Error happened 😕**\n\n`{}`\n\nPlease report this at @EDM115_chat if you think this is a serious error", 46 | "existing_thumb": "A thumbnail has already been saved 😅 What you wanna do ?\n• Check the actual thumbnail\n• Replace it with the new one you just sent\n• Cancel", 47 | "ext_failed_txt": "**Extraction failed 😕**\n\n**What to do ?**\n\n\t• Please make sure the archive isn't corrupted\n\t• Please make sure that you selected the right mode !\n\t• Also check if you sent the right password (it's case sensitive)\n\t• Maybe your archive format isn't supported yet 😔\n\n\n**⚠ IN ALL CASES ⚠**, please send **/clean**, else you can't send any other task 🙂🔫\n\nPlease report this at @EDM115_chat if you think this is a serious error", 48 | "ext_ok_txt": "**Extraction successful ✅**\n\n**Extraction time :** `{}`\n**Status :** Processing the extracted files… Please wait", 49 | "fatal_error": "Fatal error : incorrect archive format", 50 | "give_archive": "Give me an archive to extract 😐", 51 | "give_new_name": "Current file name : `{}`\n\nPlease send the new file name (**--INCLUDE THE FILE EXTENTION !--**)", 52 | "help_txt": "**• How to extract 🤔**\n\t\t\t\t**1)** Send the file or link that you want to extract\n\t\t\t\t**2)** Click on extract button (If you sent a link use `🔗` button. If it's a file just use `🗂️` button)\n\n**• How to change upload mode 🤔**\n\t\t\t\tSend **/mode**\n**Note :**\n\t\t\t\t**1.** If your archive is password protected select `🔐` button\n\t\t\t\t**2.** Please don't send corrupted files ! If you sent one by mistake just send **/clean**\n\t\t\t\t**3.** If your archive have +95 files in it then bot can't show all of extracted files to select from (yet). So in that case if you can't see your file in the buttons just click on `Upload all 📤` button. It will send all extracted files to you !\n\n**• Got an error ?**\n\t\t\t\tVisit edm115.dev/unzip#help\n\n**• I wanna have help 🥺**\n\t\t\t\tPM me at **@EDM115** or join the chat **@EDM115_chat**", 53 | "how_many_uploaded": "`{}` file(s) have been extracted from that archive", 54 | "invalid_url": "That's not a valid url 💀", 55 | "its_split": "This file is split\nUse the **/merge** command", 56 | "log_txt": "**Extract log 📝**\n\n**User ID :** `{}`\n**File name :** `{}`\n**File size :** `{}`", 57 | "maintenance_on": "Maintenance mode is currently **ON**\nTasks can't be processed. Come back later", 58 | "max_tasks": "Sorry, the bot is currently full 🥺\n\n{} tasks are already running, please wait a few minutes", 59 | "no_file_left": "There's no file left to upload", 60 | "no_merge_task": "There's no merge task ongoing\nUse **/merge** to start one", 61 | "no_space": "There's no space left on the server 😥", 62 | "not_an_archive": "That's not an archive 💀", 63 | "pass_txt": "**Password of the above archive is 🔑**\n\n`{}`", 64 | "password_protected": "That archive is password protected 😡 **/clean** and retry", 65 | "pls_send_password": "**Please send me the password 🔑**", 66 | "process_cancelled": "❌ Process cancelled", 67 | "process_merge": "Processing a user query…\n\nUser ID : {}\nTask : #Merge\n\nFile : {}", 68 | "process_msgs": "**Processing {} messages… Please wait**", 69 | "processing2": "`Processing… ⏳`", 70 | "processing_task": "**✅ Processing your task… Please wait**", 71 | "query_parse_err": "Fatal query parsing error 💀\n\nPlease contact @EDM115_chat with details and screenshots", 72 | "refresh_stats": "Refreshing stats… ♻️", 73 | "refreshing": "Refreshing… ⏳", 74 | "saved_thumbnail": "**Successfully saved this thumbnail ✅**", 75 | "select_files": "Select files to upload 👇", 76 | "send_all_parts": "Sending all parts of {} to you… Please wait", 77 | "sending_all_files": "Sending all files to you… Please wait", 78 | "spl_rz": "Split RAR/ZIP files in .rxx or .zxx format can't be processed yet", 79 | "splitting": "**Splitting {}… Please wait**", 80 | "start_text": "Hi **{}** 👋, I'm the **unzip-bot** 🥰\n\nI can extract any archive, with password or not, split, …\nSend **/commands** to learn more\n\n**Made with ❤️ by @EDM115bots**\n**/donate** if you can 🥺", 81 | "try_dl": "**Downloading… Please wait**\n", 82 | "unable_gather_files": "Unable to gather the files to upload 😥\nChoose either to upload everything, or cancel the process", 83 | "unzip_http": "Can't use unzip_http on {} : {}", 84 | "uploaded": "**Successfully uploaded ✅**\n\n**Join @EDM115bots ❤️**", 85 | "uploading_this_file": "Uploading this file… Please wait", 86 | "user_query": "Processing a user query…\n\nUser ID : {}" 87 | }, 88 | "commands": { 89 | "about_txt": "**About the unzip-bot [v{}]**\n\n• **Language :** [Python 3.12.9](https://www.python.org/)\n• **Framework :** [Mayuri-Chan/pyrofork 2.3.59](https://pyrofork.wulan17.top/)\n• **Source code :** [EDM115/unzip-bot@v7](https://github.com/EDM115/unzip-bot)\n• **Developer :** [EDM115](https://github.com/EDM115)\n\n**[Rate me ⭐](https://t.me/BotsArchive/2705)**\nMade with ❤️ by **@EDM115bots**", 90 | "admincmd": "Here's all the commands that only the owner (you) can use :\n\n**/gitpull** : Pulls the latest changes from GitHub\n**/broadcast** : Send something to all the users\n**/sendto user_id** : Same as broadcast but for a single user. Doesn't handle replies for now…\n**/ban user_id** : Ban a user. He can no longer use your bot, except if…\n**/unban user_id** : …you unban him. All his stats and settings stays saved after a ban\n**/user user_id** : Know more about the use of your bot by a single user\n**/user2 user_id** : Get full info about a [User](https://docs.pyrogram.org/api/types/User) (info returned by Pyrogram)\n**/self** : Get full info about me (info returned by Pyrogram)\n**/getthumbs** : Get all the thumbnails on the server\n**/redbutton** : __Does nothing yet__\n**/maintenance** : Put the bot in or out of maintenance mode. No tasks can be processed while on\n**/cleanall** : Same as `/clean`, but for the whole server\n**/cleantasks** : Same as `/cleanall`, plus removes them from the database\n**/logs** : Send you the logs (all of them). Useful for bug tracking. Send them to **@EDM115** if you don't understand them/need help\n**/restart** : Does a basic restart, less intrusive as the `/redbutton` one\n**/dbexport** : ~~Exports the whole database as CSV~~ __Does nothing yet__\n**/eval code** : Evaluate a piece of code. Useful for debugging\n**/exec code** : Execute a piece of code. Useful for debugging\n**/admincmd** : This message\n**/commands** : For all the other commands", 91 | "already_added": "{} is already in the user database\n\n", 92 | "already_banned": "{} has already been banned\n\n", 93 | "already_removed": "{} has already been removed from the user database", 94 | "already_unbanned": "{} has already been deleted from banned users database", 95 | "ban_id": "Give a user ID to ban 😈", 96 | "bc_done": "**Broadcast completed ✅**\n\n**Total users :** `{}`\n**Successful responses :** `{}`\n**Failed responses :** `{}`", 97 | "bc_reply": "Reply to a message to broadcast it 📡", 98 | "bc_start": "Broadcasting has started, this may take a while 😪\n\nUsers : {}/{}", 99 | "cancelled": "**Cancelled successfully ✅**", 100 | "choose_ext_mode": "Select the extraction mode for that {} 👀\n\n{} : **Normal mode**\n🔐 : **Password protected**\n🖼️ : **Change the thumbnail**\n✏ : **Change the thumbnail and rename the file**\n❌ : **Cancel your task**", 101 | "clean_txt": "**Are you sure you want to clean your task 🤔**\n\nNote : This action cannot be undone", 102 | "cleaned": "The whole server has been cleaned ✅", 103 | "commands_list": "Here is the list of the commands you can use (only in private btw) :\n\n**send any file or URL** : Prompt the extract dialog\n**/start** : To know if I'm online\n**/help** : Gives a simple help\n**/about** : Know more about me\n**/donate** : Know how you can contribute to this bot\n**/clean** : Remove your files from my server. Also useful if a task failed\n**/mode** : Change your upload mode (either `doc` or `media`)\n**/stats** : Know all the current stats about me\n**/merge** : Merge split archives together\n**/done** : After you sent all the split archives, use this to merge them\n**/info** : Get full info about a [Message](https://docs.pyrogram.org/api/types/Message) (info returned by Pyrogram)\n**/addthumb** : Upload with a custom thumbnail\n**/delthumb** : Removes your thumbnail\n**/report** : Used by replying to a message, sends it to the bot owner (useful for bug report, or any question)\n**/vip** : __Not available yet__ Know more about the VIP subscription\n**/commands** : This message", 104 | "deleted_folder": "Folder {} has been deleted successfully", 105 | "donate_text": "I'm going to be honest : **this bot costs me money**…\nNothing's free on this world, however I try to keep this bot for free for as many people as possible\nI don't like to put restrictions, nor getting your PM's flooded with ads…\n\nSo if you can, donate :)\nIt helps out a ton, covers the costs (hosting, updating, … 👨‍💻)\n\n--How ?--\n• **[Paypal](https://www.paypal.me/8EDM115)**\n• **[GitHub Sponsors](https://github.com/sponsors/EDM115)**\n• **[Directly in Telegram](https://t.me/EDM115bots/698)**\n• **[BuyMeACoffee](https://www.buymeacoffee.com/edm115)**\n\nThanks for your contribution 😊\n\n--Side note :--\nDonation doesn't count as a VIP subscription. Check **/vip** for more info", 106 | "done": "If you have sent **ALL** the files, you can click on the `Merge 🛠️` button below\n\nIf you sent /done by mistake and haven't sent all the files yet, just ignore this message and re-send **/done** after ALL the files are sent", 107 | "erase_all": "**Cleaning…**", 108 | "erase_tasks": "Deleting {} tasks… Please wait", 109 | "erase_tasks_success": "Successfully deleted {} tasks ✅", 110 | "ext_caption": "`{}`\n\nSuccessfully extracted by @unzip_edm115bot 🥰", 111 | "help_txt": "**• How to extract 🤔**\n\t\t\t\t**1)** Send the file or link that you want to extract\n\t\t\t\t**2)** Click on extract button (If you sent a link use `🔗` button. If it's a file just use `🗂️` button)\n\n**• How to change upload mode 🤔**\n\t\t\t\tSend **/mode**\n**Note :**\n\t\t\t\t**1.** If your archive is password protected select `🔐` button\n\t\t\t\t**2.** Please don't send corrupted files ! If you sent one by mistake just send **/clean**\n\t\t\t\t**3.** If your archive have +95 files in it then bot can't show all of extracted files to select from (yet). So in that case if you can't see your file in the buttons just click on `Upload all 📤` button. It will send all extracted files to you !\n\n**• Got an error ?**\n\t\t\t\tVisit edm115.dev/unzip#help\n\n**• I wanna have help 🥺**\n\t\t\t\tPM me at **@EDM115** or join the chat **@EDM115_chat**", 112 | "info": "Send a text (as short as possible) from any user/chat. And you will have infos about it 👀", 113 | "invalid": "Send a valid archive/URL", 114 | "log_sent": "Log file sent to {}", 115 | "maintenance": "Do you want the bot to go into maintenance mode 🤔\nCurrent state : `{}`", 116 | "maintenance_ask": "False : No maintenance\nTrue : Maintenance\nSend the appropriate string", 117 | "maintenance_done": "Successfully changed maintenance mode to `{}`", 118 | "maintenance_fail": "Provide one of the values", 119 | "maintenance_on": "Maintenance mode is currently **ON**\nTasks can't be processed. Come back later", 120 | "max_tasks": "Sorry, the bot is currently full 🥺\n\n{} tasks are already running, please wait a few minutes", 121 | "merge": "You have split archives to process ?\nSend me **all** the split files (.001, .002, .00×, …)\n\n**AFTER** you sent them all, send **/done** and click on the `Merge 🛠️` button", 122 | "no_pull": "Nothing to pull 😅", 123 | "no_space": "There's no space left on the server 😥", 124 | "no_thumbs": "No thumbnails on the server yet", 125 | "not_cleaned": "An error happened during /cleanall", 126 | "privacy": "PRIVACY ??", 127 | "process_running": "Already one process is running, don't spam 😐\n\nSend **/clean** if you want to process a new file", 128 | "processing2": "`Processing… ⏳`", 129 | "provide_uid": "Please provide a user ID", 130 | "provide_uid2": "Please provide a user ID/username", 131 | "pulled": "✅ Pulled changes, restarting…", 132 | "pulling": "Pulling updates… ⌛", 133 | "report_done": "Report sucessfully sent ! An answer will arrive soon\n\nNote : if you need to reply to replies, always use that /report command (or join **@EDM115_chat**)", 134 | "report_reply": "Reply to a message to report it to @EDM115", 135 | "report_text": "📢 --Report sent--\n\n**User :** `{}`\n**Message :** `{}`\n\n#Report #Action_Required", 136 | "restarted_at": "**ℹ️ Bot restarted successfully at **`{}`", 137 | "restarting": "{} : Restarting…", 138 | "select_upload_mode_txt": "Select your upload mode 👇\n\n**Current upload mode is :** `{}`", 139 | "send_failed": "It failed 😣 {} hasn't started the bot yet (or most likely deleted the chat)", 140 | "send_reply": "Reply to a message to send it 📡", 141 | "send_success": "Message successfully sent to `{}`", 142 | "sending": "Sending it, please wait… 😪", 143 | "start_text": "Hi **{}** 👋, I'm the **unzip-bot** 🥰\n\nI can extract any archive, with password or not, split, …\nSend **/commands** to learn more\n\n**Made with ❤️ by @EDM115bots**\n**/donate** if you can 🥺", 144 | "stats": "**💫 Current bot stats 💫**\n\n**💾 Disk usage :**\n ↳ **Total disk space :** `{}`\n ↳ **Used :** `{} - {}%`\n ↳ **Free :** `{}`\n ↳ **Ongoing tasks :** `{}`\n\n**🎛 Hardware usage :**\n ↳ **CPU usage :** `{}%`\n ↳ **RAM usage :** `{}%`\n ↳ **Uptime :** `{}`", 145 | "stats_owner": "**💫 Current bot stats 💫**\n\n**👥 Users :**\n ↳ **Users in database :** `{}`\n ↳ **Total banned users :** `{}`\n\n**💾 Disk usage :**\n ↳ **Total disk space :** `{}`\n ↳ **Used :** `{} - {}%`\n ↳ **Free :** `{}`\n ↳ **Ongoing tasks :** `{}`\n\n**🌐 Network usage :**\n ↳ **Uploaded :** `{}`\n ↳ **Downloaded :** `{}`\n\n**🎛 Hardware usage :**\n ↳ **CPU usage :** `{}%`\n ↳ **RAM usage :** `{}%`\n ↳ **Uptime :** `{}`", 146 | "still_starting": "The bot is still starting, please wait… 😪", 147 | "uid_uname_invalid": "An error occurred, the user ID/username is probably invalid", 148 | "unable_fetch": "Unable to fetch", 149 | "unban_id": "Give a user ID to unban 😇", 150 | "unbanned": "**Successfully unbanned that user ✅**\n\n**User ID :** `{}`", 151 | "user": "This is a WIP command that would allow you to get more stats about your utilization of me 🤓", 152 | "user2_info": "`{}`\n\n**Direct link to profile :** tg://user?id={}", 153 | "user_banned": "**Successfully banned that user ✅**\n\n**User ID :** `{}`", 154 | "user_info": "**User ID :** `{}`\n`{}` files uploaded\n…\n\nWIP", 155 | "vip_added_user": "The following user had been added with the following infos :\nUser ID : `{}`\nStart date : `{}`\nEnd date : `{}`\nPlatform : `{}`\nFrequency : `{}`\nEarly supporter : `{}`\nDonator : `{}`\nFirst subscription date : `{}`\nSuccessful payments : `{}`\nGap between payments : `{}`\nGifted : `{}`\nReferral code : `{}`\nLifetime : `{}`", 156 | "vip_info": "--**NOT AVAILABLE YET !**--\n\nWanna help the developer of this __amazing__ bot ?\nHere's how : Become a VIP user and benefit from extra perks !\n\n**VIP perks :**\n- No max tasks limit\n- No AFK timeout\n- Get a better support\n- Upload files up to 4Gb\n- Early access to new features\n- Access a second bot exclusive to VIPs __(subject to conditions)__\n- And more…\n\n**What's the price ?**\n- `1$/month`\n- `10$/year`\n\n**How to become a VIP ?**\n1) Send **/pay** to the bot\n2) Choose your subscription\n3) Send a screenshot of your payment to **@EDM115**\n4) Enjoy your VIP perks !\n\n**What happens when my subscription ends ?**\nIf you chose GitHub Sponsors, Telegram Donate or BuyMeACoffee, you will be automatically renewed until you cancel it\nIf you chose PayPal, you will have to redo the 4 above steps\nYou will be notified a few days before your subscription ends so you can check if you want to renew it or not\n\n**I wanna cancel my subscription**\nJust send **/stoppay** and follow the instructions according to the platform you selected\nYour payment will be cancelled and you will keep your VIP perks until the end of your subscription\n(i.e. if you paid for 1 month, from 05/01/2024 to 05/02/2024 and you cancel your subscription on 15/01/2024, your perks will stay until 05/02/2024)\n\n**What is the referral system ?**\nReferrals have benefits for both sides :)\n- For the referrer : you get 1 month of VIP for free for each 3 new VIPs you bring\n- For the referred : you get 1 month of VIP for free if you take the monthly subscription, and 3 months for free if you take the yearly subscription\nHow to input the referral code ? Just send **/pay** to the bot and follow the instructions", 157 | "vip_required_message": "Use this command as a reply to a messsage, where you have the following (ONE PER LINE) :\nThe user ID (int)\nWhen the subscription starts (in %Y-%m-%dT%H:%M:%SZ format)\nWhen the subscription ends (same)\nWhich platform had been used [paypal, telegram, sponsor, bmac]\nAt which frequency the user is billed [monthly, yearly]\nIs the user an early supporter (can be obtained only the first 3 months) [True, False]\nIs the user also a donator [True, False]\nWhen does the user ever started a subscription (date)\nHow many successful payments had been done (int)\nHas there been any gap between payments [True, False]\nIf the user has been gifted a Premium plan [True, False]\nThe user referral code (str)\nIs this a lifetime free subscription [True, False]" 158 | }, 159 | "custom_thumbnail": { 160 | "album": "{} tried to save a thumbnail from an album", 161 | "album_nope": "You can't use an album. Reply to a single picture sent as photo (not as document)", 162 | "del_confirm_thumb": "Do you really want to delete your thumbnail ?\n• Check the actual thumbnail\n• Delete it\n• Cancel", 163 | "dl_thumb": "Downloading thumbnail of {}…", 164 | "existing_thumb": "A thumbnail already have been saved 😅 What you wanna do ?\n• Check the actual thumbnail\n• Replace it with the new one you just sent\n• Cancel", 165 | "no_thumb": "You already have no thumbnail 😅", 166 | "pls_reply": "You need to reply ↩️ to a picture with this command for saving it as custom thumbnail 🤓", 167 | "saving_thumb": "Are you sure you want to save this thumbnail 🤔", 168 | "thumb_caption": "#thumbnail\n\nSaved thumbnail of [user {}](tg://user?id={})", 169 | "thumb_error": "Error happened 😕 Try again later", 170 | "thumb_failed": "Failed to generate thumb", 171 | "thumb_saved": "Thumbnail saved" 172 | }, 173 | "database": { 174 | "banned": "**Sorry, you're banned 💀**\n\nReport this at @EDM115_chat if you think this is a mistake, I may unban you", 175 | "new_user": "**#NEW_USER** 🎙\n\n**User profile :** `{}` {}\n**User ID :** `{}`\n**Profile URL :** [tg://user?id={}](tg://user?id={})", 176 | "new_user_bad": "**#NEW_USER** 🎙\n\n**User profile :** `{}`\n`[AttributeError]`" 177 | }, 178 | "ext_helper": { 179 | "cancel_it": "❌ Cancel", 180 | "up_all": "Upload all 📤" 181 | }, 182 | "main": { 183 | "bot_running": "Bot is running now ! Join @EDM115bots", 184 | "bot_stopped": "Bot stopped 😪", 185 | "check_log": "Checking log channel…", 186 | "error_main_loop": "Error in main loop : {}", 187 | "error_shutdown_msg": "Error sending shutdown message : {}", 188 | "log_checked": "Log channel checked", 189 | "received_stop_signal": "Received stop signal ({}, {}, {}). Exiting...", 190 | "start_txt": "ℹ️ The bot has successfully started at `{}` 💪", 191 | "starting_bot": "Starting bot…", 192 | "stop_txt": "ℹ️ The bot goes sleeping at `{}` 😴", 193 | "wrong_log": "Error : the provided **LOGS_CHANNEL** (`{}`) is incorrect\nBot crashed 😪" 194 | }, 195 | "start": { 196 | "bot_restarted": "Bot restarted !\n\n**Old boot time** : `{}`\n**New boot time** : `{}`", 197 | "dl_thumbs": "Downloading {} thumbs", 198 | "downloaded_thumbs": "Downloaded {} of {} thumbs", 199 | "error_log_check": "An error happened while checking the log channel 💀 Make sure you haven't provided a wrong log channel ID 🧐", 200 | "missing_thumb": "Due to Telegram issues, your thumbnail couldn't be downloaded.\nPlease save it again.", 201 | "no_log_id": "No log channel ID has been provided", 202 | "private_chat": "A private chat can't be used", 203 | "resend_task": "⚠️ **Warning** : the bot restarted while you were using it\nYour task was stopped, please send it again", 204 | "task_expired": "Your task was running for more than {} minutes, it has been stopped\n\nDon't go AFK next time 😉" 205 | }, 206 | "unzip_help": { 207 | "dl_stopped": "The download of your file have successfully been cancelled ✅", 208 | "eta": "**ETA :**", 209 | "processing": "**Processing…**", 210 | "progress_msg": "{}\n{}\n\n**Powered by @EDM115bots**", 211 | "speed": "**Speed :**", 212 | "unknown_size": "**Size :** Unknown\n\nThis may take a while, go grab a coffee ☕️" 213 | }, 214 | "up_helper": { 215 | "archive_gone": "Archive has gone from servers before uploading 😥", 216 | "cant_find": "Sorry ! I can't find that file {} 💀", 217 | "check_msg": "**Verifying the file… Please wait**\n\n", 218 | "empty_file": "The file {} is empty/unreachable", 219 | "ext_caption": "`{}`\n\nSuccessfully extracted by @unzip_edm115bot 🥰", 220 | "log_caption": "**The file : ** `{}`\n\nhas been saved from the URL\n\n`{}`", 221 | "processing2": "`Processing… ⏳`", 222 | "too_large": "The URL file is too large to send on Telegram 😥", 223 | "try_up": "**Uploading {}… Please wait**\n\n" 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /unzipbot/i18n/messages.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from config import Config 4 | 5 | 6 | class Messages: 7 | def __init__( 8 | self, 9 | lang_fetcher=None, 10 | default_lang=Config.BASE_LANGUAGE, 11 | base_path="unzipbot/i18n/lang", 12 | ): 13 | """ 14 | Initialize the Messages class 15 | 16 | :param lang_fetcher: A callable to fetch the user's language (takes user_id as argument) 17 | :param default_lang: The default language code (ex "en") 18 | :param base_path: The base path to the directory containing language files 19 | """ # noqa: E501 20 | self.lang_fetcher = lang_fetcher or (lambda _: default_lang) 21 | self.default_lang = default_lang 22 | self.base_path = base_path 23 | 24 | def __load_language_file(self, lang): 25 | """ 26 | Load the JSON file for the given language 27 | 28 | :param lang: Language code (ex "en") 29 | :return: Dictionary of messages 30 | """ 31 | file_path = f"{self.base_path}/{lang}.json" 32 | 33 | try: 34 | with open(file=file_path, mode="r", encoding="utf-8") as f: 35 | return json.load(fp=f) 36 | except FileNotFoundError: 37 | with open( 38 | file=f"{self.base_path}/{self.default_lang}.json", 39 | mode="r", 40 | encoding="utf-8", 41 | ) as f: 42 | return json.load(fp=f) 43 | 44 | def get(self, file, key, user_id=None, extra_args=[]): 45 | """ 46 | Retrieve and format a message by its file and key 47 | 48 | :param file: The name of the file in the JSON structure 49 | :param key: The key within the file to retrieve 50 | :param user_id: The user's ID (used to fetch the preferred language) 51 | :param extra_args: Additional arguments for string formatting 52 | :return: The formatted message string 53 | """ 54 | lang = self.lang_fetcher(user_id) if user_id else self.default_lang 55 | messages = self.__load_language_file(lang) 56 | 57 | try: 58 | message = messages[file][key.lower()] 59 | except KeyError: 60 | message = self.__load_language_file(self.default_lang)[file][key.lower()] 61 | 62 | if not isinstance(extra_args, list): 63 | extra_args = [extra_args] 64 | 65 | return message.format(*extra_args) 66 | -------------------------------------------------------------------------------- /unzipbot/modules/ext_script/custom_thumbnail.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from asyncio import sleep 4 | 5 | from PIL import Image 6 | from pyrogram.errors import FloodPremiumWait, FloodWait 7 | 8 | from config import Config 9 | from unzipbot import LOGGER 10 | from unzipbot.helpers.database import get_lang, update_temp_thumb 11 | from unzipbot.i18n.buttons import Buttons 12 | from unzipbot.i18n.messages import Messages 13 | 14 | messages = Messages(lang_fetcher=get_lang) 15 | 16 | 17 | async def silent_del(user_id): 18 | try: 19 | thumb_location = Config.THUMB_LOCATION + "/" + str(user_id) + ".jpg" 20 | os.remove(path=thumb_location) 21 | except: 22 | pass 23 | 24 | 25 | async def add_thumb(_, message): 26 | try: 27 | uid = message.from_user.id 28 | user_id = str(uid) 29 | 30 | if message.reply_to_message is not None: 31 | reply_message = message.reply_to_message 32 | 33 | if reply_message.media_group_id is not None: # album sent 34 | LOGGER.info( 35 | msg=messages.get( 36 | file="custom_thumbnail", key="ALBUM", extra_args=user_id 37 | ) 38 | ) 39 | await message.reply( 40 | messages.get(file="custom_thumbnail", key="ALBUM_NOPE", user_id=uid) 41 | ) 42 | 43 | return 44 | 45 | thumb_location = Config.THUMB_LOCATION + "/" + user_id + ".jpg" 46 | pre_thumb = Config.THUMB_LOCATION + "/not_resized_" + user_id + ".jpg" 47 | final_thumb = Config.THUMB_LOCATION + "/waiting_" + user_id + ".jpg" 48 | LOGGER.info( 49 | msg=messages.get( 50 | file="custom_thumbnail", key="DL_THUMB", extra_args=user_id 51 | ) 52 | ) 53 | file = await _.download_media(message=reply_message) 54 | shutil.move(src=file, dst=pre_thumb) 55 | size = (320, 320) 56 | 57 | try: 58 | with Image.open(fp=pre_thumb) as previous: 59 | previous.thumbnail(size=size, resample=Image.Resampling.LANCZOS) 60 | previous.save(fp=final_thumb, format="JPEG") 61 | LOGGER.info( 62 | msg=messages.get(file="custom_thumbnail", key="THUMB_SAVED") 63 | ) 64 | savedpic = await _.send_photo( 65 | chat_id=Config.LOGS_CHANNEL, 66 | photo=final_thumb, 67 | caption=messages.get( 68 | file="custom_thumbnail", 69 | key="THUMB_CAPTION", 70 | user_id=uid, 71 | extra_args=[user_id, user_id], 72 | ), 73 | ) 74 | 75 | try: 76 | os.remove(path=pre_thumb) 77 | except: 78 | pass 79 | 80 | await update_temp_thumb( 81 | user_id=message.from_user.id, thumb_id=savedpic.photo.file_id 82 | ) 83 | 84 | if os.path.exists(thumb_location) and os.path.isfile(thumb_location): 85 | await message.reply( 86 | text=messages.get( 87 | file="custom_thumbnail", key="EXISTING_THUMB", user_id=uid 88 | ), 89 | reply_markup=Buttons.THUMB_REPLACEMENT, 90 | ) 91 | else: 92 | await message.reply( 93 | text=messages.get( 94 | file="custom_thumbnail", key="SAVING_THUMB", user_id=uid 95 | ), 96 | reply_markup=Buttons.THUMB_SAVE, 97 | ) 98 | except: 99 | LOGGER.info( 100 | msg=messages.get(file="custom_thumbnail", key="THUMB_FAILED") 101 | ) 102 | 103 | try: 104 | os.remove(path=final_thumb) 105 | except: 106 | pass 107 | 108 | await message.reply( 109 | messages.get( 110 | file="custom_thumbnail", key="THUMB_ERROR", user_id=uid 111 | ) 112 | ) 113 | else: 114 | await _.send_message( 115 | chat_id=message.chat.id, 116 | text=messages.get( 117 | file="custom_thumbnail", key="PLS_REPLY", user_id=uid 118 | ), 119 | reply_to_message_id=message.id, 120 | ) 121 | except (FloodWait, FloodPremiumWait) as f: 122 | await sleep(f.value) 123 | await add_thumb(_=_, message=message) 124 | 125 | 126 | async def del_thumb(message): 127 | try: 128 | uid = message.from_user.id 129 | thumb_location = Config.THUMB_LOCATION + "/" + str(uid) + ".jpg" 130 | 131 | if not os.path.exists(thumb_location): 132 | await message.reply( 133 | text=messages.get(file="custom_thumbnail", key="NO_THUMB", user_id=uid) 134 | ) 135 | else: 136 | await message.reply( 137 | text=messages.get( 138 | file="custom_thumbnail", key="DEL_CONFIRM_THUMB", user_id=uid 139 | ), 140 | reply_markup=Buttons.THUMB_DEL, 141 | ) 142 | except (FloodWait, FloodPremiumWait) as f: 143 | await sleep(f.value) 144 | await del_thumb(message) 145 | 146 | 147 | async def thumb_exists(chat_id): 148 | thumb_location = Config.THUMB_LOCATION + "/" + str(chat_id) + ".jpg" 149 | 150 | return os.path.exists(thumb_location) 151 | -------------------------------------------------------------------------------- /unzipbot/modules/ext_script/ext_helper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from asyncio import create_subprocess_shell, subprocess 4 | from shlex import quote 5 | 6 | from pykeyboard import InlineKeyboard 7 | from pyrogram.types import InlineKeyboardButton 8 | 9 | from config import Config 10 | from unzipbot import LOGGER 11 | from unzipbot.helpers.database import get_lang 12 | from unzipbot.helpers.unzip_help import calculate_memory_limit, tarball_extensions 13 | from unzipbot.i18n.messages import Messages 14 | 15 | messages = Messages(lang_fetcher=get_lang) 16 | 17 | 18 | # Get files in directory as a list 19 | async def get_files(path): 20 | path_list = [ 21 | val 22 | for sublist in [ 23 | [os.path.join(i[0], j) for j in i[2]] for i in os.walk(top=path) 24 | ] 25 | for val in sublist 26 | ] 27 | 28 | return sorted(path_list) 29 | 30 | 31 | async def cleanup_macos_artifacts(extraction_path): 32 | for root, dirs, files in os.walk(top=extraction_path): 33 | for name in files: 34 | if name == ".DS_Store": 35 | os.remove(path=os.path.join(root, name)) 36 | for name in dirs: 37 | if name == "__MACOSX": 38 | shutil.rmtree(os.path.join(root, name)) 39 | 40 | 41 | async def run_shell_cmds(command): 42 | memlimit = calculate_memory_limit() 43 | cpulimit = Config.MAX_CPU_CORES_COUNT * Config.MAX_CPU_USAGE 44 | ulimit_cmd = [ 45 | "ulimit", 46 | "-v", 47 | str(memlimit), 48 | "&&", 49 | "cpulimit", 50 | "-l", 51 | str(cpulimit), 52 | "--", 53 | command, 54 | ] 55 | ulimit_command = " ".join(ulimit_cmd) 56 | process = await create_subprocess_shell( 57 | cmd=ulimit_command, 58 | stdout=subprocess.PIPE, 59 | stderr=subprocess.PIPE, 60 | executable="/bin/bash", 61 | ) 62 | stdout, stderr = await process.communicate() 63 | 64 | e = stderr.decode(encoding="utf-8", errors="replace") 65 | o = stdout.decode(encoding="utf-8", errors="replace") 66 | LOGGER.info(msg=f"command : {command}") 67 | LOGGER.info(msg=f"stdout : {o}") 68 | LOGGER.info(msg=f"stderr : {e}") 69 | 70 | return o + "\n" + e 71 | 72 | 73 | # Extract with 7z 74 | async def __extract_with_7z_helper(path, archive_path, password=None): 75 | LOGGER.info(msg="7z : " + archive_path + " : " + path) 76 | 77 | if password: 78 | cmd = [ 79 | "7z", 80 | "x", 81 | f"-o{quote(path)}", 82 | f"-p{quote(password)}", 83 | quote(archive_path), 84 | "-y", 85 | ] 86 | else: 87 | cmd = ["7z", "x", f"-o{quote(path)}", quote(archive_path), "-y"] 88 | 89 | result = await run_shell_cmds(" ".join(cmd)) 90 | 91 | return result 92 | 93 | 94 | async def test_with_7z_helper(archive_path): 95 | # skipcq: PTC-W1006, SCT-A000 96 | password = "dont care + didnt ask + cry about it + stay mad + get real + L" 97 | cmd = ["7z", "t", f"-p{quote(password)}", quote(archive_path), "-y"] 98 | result = await run_shell_cmds(" ".join(cmd)) 99 | 100 | return "Everything is Ok" in result 101 | 102 | 103 | async def __extract_with_unrar_helper(path, archive_path, password=None): 104 | LOGGER.info(msg="unrar : " + archive_path + " : " + path) 105 | 106 | if password: 107 | cmd = [ 108 | "unrar", 109 | "x", 110 | quote(archive_path), 111 | quote(path), 112 | f"-p{quote(password)}", 113 | "-y", 114 | ] 115 | else: 116 | cmd = ["unrar", "x", quote(archive_path), quote(path), "-y"] 117 | 118 | result = await run_shell_cmds(" ".join(cmd)) 119 | 120 | return result 121 | 122 | 123 | async def test_with_unrar_helper(archive_path): 124 | # skipcq: PTC-W1006, SCT-A000 125 | password = "dont care + didnt ask + cry about it + stay mad + get real + L" 126 | cmd = ["unrar", "t", quote(archive_path), f"-p{quote(password)}", "-y"] 127 | result = await run_shell_cmds(" ".join(cmd)) 128 | 129 | return "All OK" in result 130 | 131 | 132 | # Extract with zstd (for .tar.zst files) 133 | async def __extract_with_zstd(path, archive_path): 134 | cmd = ["zstd", "-f", "--output-dir-flat", quote(path), "-d", quote(archive_path)] 135 | result = await run_shell_cmds(" ".join(cmd)) 136 | 137 | return result 138 | 139 | 140 | # Main function to extract files 141 | async def extr_files(path, archive_path, password=None): 142 | os.makedirs(name=path, exist_ok=True) 143 | 144 | if archive_path.endswith(tarball_extensions): 145 | LOGGER.info(msg="tar") 146 | temp_path = path.rsplit("/", 1)[0] + "/tar_temp" 147 | os.makedirs(name=temp_path, exist_ok=True) 148 | result = await __extract_with_7z_helper( 149 | path=temp_path, archive_path=archive_path 150 | ) 151 | filename = await get_files(temp_path) 152 | filename = filename[0] 153 | cmd = ["tar", "-xvf", quote(filename), "-C", quote(path)] 154 | result2 = await run_shell_cmds(" ".join(cmd)) 155 | result += result2 156 | shutil.rmtree(temp_path) 157 | elif archive_path.endswith((".tar.zst", ".zst", ".tzst")): 158 | LOGGER.info(msg="zstd") 159 | os.mkdir(path=path) 160 | result = await __extract_with_zstd(path=path, archive_path=archive_path) 161 | elif archive_path.endswith(".rar"): 162 | LOGGER.info(msg="rar") 163 | 164 | if password: 165 | result = await __extract_with_unrar_helper( 166 | path=path, archive_path=archive_path, password=password 167 | ) 168 | else: 169 | result = await __extract_with_unrar_helper( 170 | path=path, archive_path=archive_path 171 | ) 172 | else: 173 | LOGGER.info(msg="normal archive") 174 | result = await __extract_with_7z_helper( 175 | path=path, archive_path=archive_path, password=password 176 | ) 177 | 178 | LOGGER.info(msg=await get_files(path)) 179 | await cleanup_macos_artifacts(path) 180 | 181 | return result 182 | 183 | 184 | # Split files 185 | async def split_files(iinput, ooutput, size): 186 | temp_location = iinput + "_temp" 187 | shutil.move(src=iinput, dst=temp_location) 188 | cmd = [ 189 | "7z", 190 | "a", 191 | "-tzip", 192 | "-mx=0", 193 | quote(ooutput), 194 | quote(temp_location), 195 | f"-v{size}b", 196 | ] 197 | await run_shell_cmds(" ".join(cmd)) 198 | spdir = ooutput.replace("/" + ooutput.split("/")[-1], "") 199 | files = await get_files(spdir) 200 | 201 | return files 202 | 203 | 204 | # Merge files 205 | async def merge_files(iinput, ooutput, file_type, password=None): 206 | if file_type == "volume": 207 | result = await __extract_with_7z_helper( 208 | path=ooutput, archive_path=iinput, password=password 209 | ) 210 | elif file_type == "rar": 211 | result = await __extract_with_unrar_helper( 212 | path=ooutput, archive_path=iinput, password=password 213 | ) 214 | 215 | return result 216 | 217 | 218 | # Make keyboard 219 | async def make_keyboard(paths, user_id, chat_id, unziphttp, rzfile=None): 220 | num = 0 221 | i_kbd = InlineKeyboard(row_width=1) 222 | data = [] 223 | 224 | if unziphttp: 225 | data.append( 226 | InlineKeyboardButton( 227 | text=messages.get(file="ext_helper", key="UP_ALL", user_id=user_id), 228 | callback_data=f"ext_a|{user_id}|{chat_id}|{unziphttp}|{rzfile}", 229 | ) 230 | ) 231 | else: 232 | data.append( 233 | InlineKeyboardButton( 234 | text=messages.get(file="ext_helper", key="UP_ALL", user_id=user_id), 235 | callback_data=f"ext_a|{user_id}|{chat_id}|{unziphttp}", 236 | ) 237 | ) 238 | 239 | data.append( 240 | InlineKeyboardButton( 241 | text=messages.get(file="ext_helper", key="CANCEL_IT", user_id=user_id), 242 | callback_data="cancel_dis", 243 | ) 244 | ) 245 | 246 | for file in paths: 247 | if num > 96: 248 | break 249 | 250 | if unziphttp: 251 | data.append( 252 | InlineKeyboardButton( 253 | text=f"{num} - {os.path.basename(file)}".encode( 254 | encoding="utf-8", errors="surrogateescape" 255 | ).decode(encoding="utf-8", errors="surrogateescape"), 256 | callback_data=f"ext_f|{user_id}|{chat_id}|{num}|{unziphttp}|{rzfile}", 257 | ) 258 | ) 259 | else: 260 | data.append( 261 | InlineKeyboardButton( 262 | text=f"{num} - {os.path.basename(file)}".encode( 263 | encoding="utf-8", errors="surrogateescape" 264 | ).decode(encoding="utf-8", errors="surrogateescape"), 265 | callback_data=f"ext_f|{user_id}|{chat_id}|{num}|{unziphttp}", 266 | ) 267 | ) 268 | 269 | num += 1 270 | 271 | i_kbd.add(*data) 272 | 273 | return i_kbd 274 | 275 | 276 | async def make_keyboard_empty(user_id, chat_id, unziphttp, rzfile=None): 277 | i_kbd = InlineKeyboard(row_width=2) 278 | data = [] 279 | 280 | if unziphttp: 281 | data.append( 282 | InlineKeyboardButton( 283 | text=messages.get(file="ext_helper", key="UP_ALL", user_id=user_id), 284 | callback_data=f"ext_a|{user_id}|{chat_id}|{unziphttp}|{rzfile}", 285 | ) 286 | ) 287 | else: 288 | data.append( 289 | InlineKeyboardButton( 290 | text=messages.get(file="ext_helper", key="UP_ALL", user_id=user_id), 291 | callback_data=f"ext_a|{user_id}|{chat_id}|{unziphttp}", 292 | ) 293 | ) 294 | 295 | data.append( 296 | InlineKeyboardButton( 297 | text=messages.get(file="ext_helper", key="CANCEL_IT", user_id=user_id), 298 | callback_data="cancel_dis", 299 | ) 300 | ) 301 | 302 | i_kbd.add(*data) 303 | 304 | return i_kbd 305 | -------------------------------------------------------------------------------- /unzipbot/modules/ext_script/metadata_helper.py: -------------------------------------------------------------------------------- 1 | import os 2 | from shlex import join 3 | 4 | import mutagen.id3 as id3 5 | from mutagen.aac import AAC 6 | from mutagen.aiff import AIFF 7 | from mutagen.asf import ASF 8 | from mutagen.easyid3 import EasyID3 9 | from mutagen.flac import FLAC 10 | from mutagen.mp3 import MP3 11 | from mutagen.mp4 import MP4 12 | from mutagen.oggopus import OggOpus 13 | from mutagen.oggvorbis import OggVorbis 14 | from mutagen.wave import WAVE 15 | 16 | from unzipbot.modules.ext_script.ext_helper import run_shell_cmds 17 | 18 | 19 | async def get_audio_metadata(file_path): 20 | file_ext = file_path.split(".")[-1].lower() 21 | audio_meta = {"performer": None, "title": None, "duration": None} 22 | 23 | try: 24 | if file_ext in ["mp3"]: 25 | audio = MP3(file_path, ID3=EasyID3) 26 | elif file_ext in ["m4a", "alac"]: 27 | audio = MP4(file_path) 28 | elif file_ext in ["flac"]: 29 | audio = FLAC(file_path) 30 | elif file_ext in ["aif", "aiff"]: 31 | audio = AIFF(file_path) 32 | elif file_ext in ["ogg"]: 33 | audio = OggVorbis(file_path) 34 | elif file_ext in ["opus"]: 35 | audio = OggOpus(file_path) 36 | elif file_ext in ["wav"]: 37 | audio = WAVE(file_path) 38 | elif file_ext in ["wma"]: 39 | audio = ASF(file_path) 40 | elif file_ext in ["aac"]: 41 | audio = AAC(file_path) 42 | else: 43 | return audio_meta 44 | 45 | audio_meta["duration"] = int(audio.info.length) 46 | 47 | if file_ext == "mp3": 48 | audio_meta["performer"] = audio.get(key="artist", default=[None])[0] 49 | audio_meta["title"] = audio.get(key="title", default=[None])[0] 50 | 51 | elif file_ext in ["m4a", "alac"]: 52 | audio_meta["performer"] = audio.tags.get(key="\xa9ART", default=[None])[0] 53 | audio_meta["title"] = audio.tags.get(key="\xa9nam", default=[None])[0] 54 | 55 | elif file_ext == "flac": 56 | audio_meta["performer"] = audio.get(key="artist", default=[None])[0] 57 | audio_meta["title"] = audio.get(key="title", default=[None])[0] 58 | 59 | elif file_ext in ["aif", "aiff"]: 60 | audio_meta["performer"] = audio.get(key="artist", default=[None])[0] 61 | audio_meta["title"] = audio.get(key="title", default=[None])[0] 62 | 63 | elif file_ext == "ogg": 64 | audio_meta["performer"] = audio.get(key="artist", default=[None])[0] 65 | audio_meta["title"] = audio.get(key="title", default=[None])[0] 66 | 67 | elif file_ext == "opus": 68 | audio_meta["performer"] = audio.get(key="artist", default=[None])[0] 69 | audio_meta["title"] = audio.get(key="title", default=[None])[0] 70 | 71 | elif file_ext == "wav": 72 | # WAV doesn't have a standard tagging system, handling might vary 73 | pass 74 | 75 | elif file_ext == "wma": 76 | audio_meta["performer"] = audio.tags.get(key="Author", default=[None])[0] 77 | audio_meta["title"] = audio.tags.get(key="WM/AlbumTitle", default=[None])[0] 78 | 79 | elif file_ext == "aac": 80 | # AAC tagging is not standardized, handling might vary 81 | pass 82 | 83 | except Exception: 84 | return audio_meta 85 | 86 | return audio_meta 87 | 88 | 89 | async def convert_and_save(file_path, target_format, metadata): 90 | directory, filename = os.path.split(file_path) 91 | basename, _ = os.path.splitext(filename) 92 | new_file = os.path.join(directory, f"{basename}.{target_format}") 93 | 94 | cmd = ["ffmpeg", "-i", file_path, "-vn", new_file] 95 | await run_shell_cmds(join(cmd)) 96 | 97 | if target_format == "mp3": 98 | audio = MP3(new_file, ID3=EasyID3) 99 | audio["artist"] = metadata["performer"] 100 | audio["title"] = metadata["title"] 101 | audio.save() 102 | elif target_format in ["m4a", "alac"]: 103 | audio = MP4(new_file) 104 | audio.tags["\xa9ART"] = metadata["performer"] 105 | audio.tags["\xa9nam"] = metadata["title"] 106 | audio.save() 107 | elif target_format == "flac": 108 | audio = FLAC(new_file) 109 | audio["artist"] = metadata["performer"] 110 | audio["title"] = metadata["title"] 111 | audio.save() 112 | elif target_format in ["aif", "aiff"]: 113 | audio = AIFF(new_file) 114 | # The metadata have to be a Frame instance 115 | audio["artist"] = id3.TextFrame(encoding=3, text=[metadata["performer"]]) 116 | audio["title"] = id3.TextFrame(encoding=3, text=[metadata["title"]]) 117 | audio.save() 118 | elif target_format == "ogg": 119 | audio = OggVorbis(new_file) 120 | audio["artist"] = metadata["performer"] 121 | audio["title"] = metadata["title"] 122 | audio.save() 123 | elif target_format == "opus": 124 | audio = OggOpus(new_file) 125 | audio["artist"] = metadata["performer"] 126 | audio["title"] = metadata["title"] 127 | audio.save() 128 | elif target_format == "wav": 129 | audio = WAVE(new_file) 130 | audio.save() 131 | elif target_format == "wma": 132 | audio = ASF(new_file) 133 | audio.tags["Author"] = metadata["performer"] 134 | audio.tags["WM/AlbumTitle"] = metadata["title"] 135 | audio.save() 136 | elif target_format == "aac": 137 | audio = AAC(new_file) 138 | audio.save() 139 | 140 | return new_file 141 | -------------------------------------------------------------------------------- /unzipbot/modules/ext_script/up_helper.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import pathlib 4 | import re 5 | import shutil 6 | from datetime import timedelta 7 | from shlex import quote 8 | from time import time 9 | 10 | from pyrogram.errors import ( 11 | FloodPremiumWait, 12 | FloodWait, 13 | PhotoExtInvalid, 14 | PhotoSaveFileInvalid, 15 | ) 16 | 17 | from config import Config 18 | from unzipbot import LOGGER, unzipbot_client 19 | from unzipbot.helpers.database import get_lang, get_upload_mode 20 | from unzipbot.helpers.unzip_help import ( 21 | extentions_list, 22 | progress_for_pyrogram, 23 | progress_urls, 24 | ) 25 | from unzipbot.i18n.messages import Messages 26 | from unzipbot.modules.ext_script.custom_thumbnail import thumb_exists 27 | from unzipbot.modules.ext_script.ext_helper import run_shell_cmds 28 | from unzipbot.modules.ext_script.metadata_helper import get_audio_metadata 29 | 30 | messages = Messages(lang_fetcher=get_lang) 31 | 32 | 33 | # Get file size 34 | async def get_size(doc_f): 35 | try: 36 | fsize = os.stat(path=doc_f).st_size 37 | 38 | return fsize 39 | except: 40 | return -1 41 | 42 | 43 | # Send file to a user 44 | async def send_file(unzip_bot, c_id, doc_f, query, full_path, log_msg, split): 45 | fsize = await get_size(doc_f) 46 | 47 | if fsize in (-1, 0): # File not found or empty 48 | try: 49 | await unzipbot_client.send_message( 50 | chat_id=c_id, 51 | text=messages.get( 52 | file="up_helper", 53 | key="EMPTY_FILE", 54 | user_id=c_id, 55 | extra_args=os.path.basename(doc_f), 56 | ), 57 | ) 58 | except: 59 | pass 60 | 61 | return 62 | 63 | try: 64 | ul_mode = await get_upload_mode(c_id) 65 | fname = os.sep.join(os.path.abspath(doc_f).split(os.sep)[5:]) 66 | fext = (pathlib.Path(os.path.abspath(doc_f)).suffix).casefold().replace(".", "") 67 | thumbornot = await thumb_exists(c_id) 68 | 69 | if fsize > Config.MIN_SIZE_PROGRESS: 70 | upmsg = await unzipbot_client.send_message( 71 | chat_id=c_id, 72 | text=messages.get(file="up_helper", key="PROCESSING2", user_id=c_id), 73 | disable_notification=True, 74 | ) 75 | else: 76 | upmsg = None 77 | 78 | if ul_mode == "media" and fext in extentions_list["audio"]: 79 | metadata = await get_audio_metadata(doc_f) 80 | 81 | if thumbornot: 82 | thumb_image = Config.THUMB_LOCATION + "/" + str(c_id) + ".jpg" 83 | await unzip_bot.send_audio( 84 | chat_id=c_id, 85 | audio=doc_f, 86 | caption=messages.get( 87 | file="up_helper", 88 | key="EXT_CAPTION", 89 | user_id=c_id, 90 | extra_args=fname, 91 | ), 92 | duration=metadata["duration"], 93 | performer=metadata["performer"], 94 | title=metadata["title"], 95 | thumb=thumb_image, 96 | disable_notification=True, 97 | progress=progress_for_pyrogram, 98 | progress_args=( 99 | messages.get( 100 | file="up_helper", 101 | key="TRY_UP", 102 | user_id=c_id, 103 | extra_args=fname, 104 | ), 105 | upmsg, 106 | time(), 107 | unzip_bot, 108 | ), 109 | ) 110 | else: 111 | await unzip_bot.send_audio( 112 | chat_id=c_id, 113 | audio=doc_f, 114 | caption=messages.get( 115 | file="up_helper", 116 | key="EXT_CAPTION", 117 | user_id=c_id, 118 | extra_args=fname, 119 | ), 120 | duration=metadata["duration"], 121 | performer=metadata["performer"], 122 | title=metadata["title"], 123 | disable_notification=True, 124 | progress=progress_for_pyrogram, 125 | progress_args=( 126 | messages.get( 127 | file="up_helper", 128 | key="TRY_UP", 129 | user_id=c_id, 130 | extra_args=fname, 131 | ), 132 | upmsg, 133 | time(), 134 | unzip_bot, 135 | ), 136 | ) 137 | 138 | elif ul_mode == "media" and fext in extentions_list["photo"]: 139 | # impossible to use a thumb here :( 140 | try: 141 | await unzip_bot.send_photo( 142 | chat_id=c_id, 143 | photo=doc_f, 144 | caption=messages.get( 145 | file="up_helper", 146 | key="EXT_CAPTION", 147 | user_id=c_id, 148 | extra_args=fname, 149 | ), 150 | disable_notification=True, 151 | progress=progress_for_pyrogram, 152 | progress_args=( 153 | messages.get( 154 | file="up_helper", 155 | key="TRY_UP", 156 | user_id=c_id, 157 | extra_args=fname, 158 | ), 159 | upmsg, 160 | time(), 161 | unzip_bot, 162 | ), 163 | ) 164 | except (PhotoExtInvalid, PhotoSaveFileInvalid): 165 | if thumbornot: 166 | thumb_image = Config.THUMB_LOCATION + "/" + str(c_id) + ".jpg" 167 | await unzip_bot.send_document( 168 | chat_id=c_id, 169 | document=doc_f, 170 | thumb=thumb_image, 171 | caption=messages.get( 172 | file="up_helper", 173 | key="EXT_CAPTION", 174 | user_id=c_id, 175 | extra_args=fname, 176 | ), 177 | force_document=True, 178 | disable_notification=True, 179 | progress=progress_for_pyrogram, 180 | progress_args=( 181 | messages.get( 182 | file="up_helper", 183 | key="TRY_UP", 184 | user_id=c_id, 185 | extra_args=fname, 186 | ), 187 | upmsg, 188 | time(), 189 | unzip_bot, 190 | ), 191 | ) 192 | else: 193 | await unzip_bot.send_document( 194 | chat_id=c_id, 195 | document=doc_f, 196 | caption=messages.get( 197 | file="up_helper", 198 | key="EXT_CAPTION", 199 | user_id=c_id, 200 | extra_args=fname, 201 | ), 202 | force_document=True, 203 | disable_notification=True, 204 | progress=progress_for_pyrogram, 205 | progress_args=( 206 | messages.get( 207 | file="up_helper", 208 | key="TRY_UP", 209 | user_id=c_id, 210 | extra_args=fname, 211 | ), 212 | upmsg, 213 | time(), 214 | unzip_bot, 215 | ), 216 | ) 217 | 218 | elif ul_mode == "media" and fext in extentions_list["video"]: 219 | cmd = [ 220 | "ffprobe", 221 | "-v", 222 | "error", 223 | "-show_entries", 224 | "format=duration", 225 | "-of", 226 | "default=noprint_wrappers=1:nokey=1", 227 | quote(doc_f), 228 | ] 229 | result = await run_shell_cmds(" ".join(cmd)) 230 | vid_duration = int(float(result.strip())) 231 | 232 | if thumbornot: 233 | thumb_image = Config.THUMB_LOCATION + "/" + str(c_id) + ".jpg" 234 | await unzip_bot.send_video( 235 | chat_id=c_id, 236 | video=doc_f, 237 | caption=messages.get( 238 | file="up_helper", 239 | key="EXT_CAPTION", 240 | user_id=c_id, 241 | extra_args=fname, 242 | ), 243 | duration=vid_duration, 244 | thumb=thumb_image, 245 | disable_notification=True, 246 | progress=progress_for_pyrogram, 247 | progress_args=( 248 | messages.get( 249 | file="up_helper", 250 | key="TRY_UP", 251 | user_id=c_id, 252 | extra_args=fname, 253 | ), 254 | upmsg, 255 | time(), 256 | unzip_bot, 257 | ), 258 | ) 259 | else: 260 | thmb_pth = ( 261 | f"{Config.THUMB_LOCATION}/thumbnail_{os.path.basename(doc_f)}.jpg" 262 | ) 263 | 264 | if os.path.exists(thmb_pth): 265 | os.remove(path=thmb_pth) 266 | 267 | midpoint_seconds = int(vid_duration / 2) 268 | midpoint_timedelta = timedelta(seconds=midpoint_seconds) 269 | midpoint_str = str(midpoint_timedelta) 270 | 271 | if "." not in midpoint_str: 272 | midpoint_str += ".00" 273 | else: 274 | midpoint_str = ( 275 | midpoint_str.split(sep=".")[0] 276 | + "." 277 | + midpoint_str.split(sep=".")[1][:2] 278 | ) 279 | 280 | cmd = [ 281 | "ffmpeg", 282 | "-ss", 283 | midpoint_str, 284 | "-i", 285 | quote(doc_f), 286 | "-vf", 287 | "scale=320:320:force_original_aspect_ratio=decrease", 288 | "-vframes", 289 | "1", 290 | quote(thmb_pth), 291 | ] 292 | await run_shell_cmds(" ".join(cmd)) 293 | 294 | if not os.path.exists(thmb_pth): 295 | shutil.copy(src=Config.BOT_THUMB, dst=thmb_pth) 296 | 297 | await unzip_bot.send_video( 298 | chat_id=c_id, 299 | video=doc_f, 300 | caption=messages.get( 301 | file="up_helper", 302 | key="EXT_CAPTION", 303 | user_id=c_id, 304 | extra_args=fname, 305 | ), 306 | duration=vid_duration, 307 | thumb=thmb_pth, 308 | disable_notification=True, 309 | progress=progress_for_pyrogram, 310 | progress_args=( 311 | messages.get( 312 | file="up_helper", 313 | key="TRY_UP", 314 | user_id=c_id, 315 | extra_args=fname, 316 | ), 317 | upmsg, 318 | time(), 319 | unzip_bot, 320 | ), 321 | ) 322 | 323 | try: 324 | os.remove(path=thmb_pth) 325 | except: 326 | pass 327 | 328 | else: 329 | if thumbornot: 330 | thumb_image = Config.THUMB_LOCATION + "/" + str(c_id) + ".jpg" 331 | await unzip_bot.send_document( 332 | chat_id=c_id, 333 | document=doc_f, 334 | thumb=thumb_image, 335 | caption=messages.get( 336 | file="up_helper", 337 | key="EXT_CAPTION", 338 | user_id=c_id, 339 | extra_args=fname, 340 | ), 341 | force_document=True, 342 | disable_notification=True, 343 | progress=progress_for_pyrogram, 344 | progress_args=( 345 | messages.get( 346 | file="up_helper", 347 | key="TRY_UP", 348 | user_id=c_id, 349 | extra_args=fname, 350 | ), 351 | upmsg, 352 | time(), 353 | unzip_bot, 354 | ), 355 | ) 356 | else: 357 | await unzip_bot.send_document( 358 | chat_id=c_id, 359 | document=doc_f, 360 | caption=messages.get( 361 | file="up_helper", 362 | key="EXT_CAPTION", 363 | user_id=c_id, 364 | extra_args=fname, 365 | ), 366 | force_document=True, 367 | disable_notification=True, 368 | progress=progress_for_pyrogram, 369 | progress_args=( 370 | messages.get( 371 | file="up_helper", 372 | key="TRY_UP", 373 | user_id=c_id, 374 | extra_args=fname, 375 | ), 376 | upmsg, 377 | time(), 378 | unzip_bot, 379 | ), 380 | ) 381 | 382 | if upmsg: 383 | await upmsg.delete() 384 | 385 | os.remove(path=doc_f) 386 | except (FloodWait, FloodPremiumWait) as f: 387 | await asyncio.sleep(f.value) 388 | await send_file( 389 | unzip_bot=unzip_bot, 390 | c_id=c_id, 391 | doc_f=doc_f, 392 | query=query, 393 | full_path=full_path, 394 | log_msg=log_msg, 395 | split=split, 396 | ) 397 | except FileNotFoundError: 398 | try: 399 | await unzipbot_client.send_message( 400 | chat_id=c_id, 401 | text=messages.get( 402 | file="up_helper", 403 | key="CANT_FIND", 404 | user_id=c_id, 405 | extra_args=os.path.basename(doc_f), 406 | ), 407 | ) 408 | except: 409 | pass 410 | 411 | return 412 | except BaseException as e: 413 | LOGGER.error(msg=e) 414 | shutil.rmtree(full_path) 415 | 416 | 417 | async def forward_file(message, cid): 418 | try: 419 | await unzipbot_client.copy_message( 420 | chat_id=cid, 421 | from_chat_id=message.chat.id, 422 | message_id=message.id, 423 | disable_notification=True, 424 | ) 425 | except (FloodWait, FloodPremiumWait) as f: 426 | await asyncio.sleep(f.value) 427 | await forward_file(message=message, cid=cid) 428 | 429 | 430 | async def send_url_logs(unzip_bot, c_id, doc_f, source, message): 431 | try: 432 | u_file_size = os.stat(path=doc_f).st_size 433 | 434 | if Config.TG_MAX_SIZE < int(u_file_size): 435 | await unzip_bot.send_message( 436 | chat_id=c_id, 437 | text=messages.get(file="up_helper", key="TOO_LARGE", user_id=c_id), 438 | ) 439 | 440 | return 441 | 442 | fname = os.path.basename(doc_f) 443 | await unzip_bot.send_document( 444 | chat_id=c_id, 445 | document=doc_f, 446 | caption=messages.get( 447 | file="up_helper", 448 | key="LOG_CAPTION", 449 | user_id=c_id, 450 | extra_args=[fname, source], 451 | ), 452 | disable_notification=True, 453 | progress=progress_urls, 454 | progress_args=( 455 | messages.get(file="up_helper", key="CHECK_MSG", user_id=c_id), 456 | message, 457 | time(), 458 | ), 459 | ) 460 | except (FloodWait, FloodPremiumWait) as f: 461 | await asyncio.sleep(f.value) 462 | 463 | return send_url_logs( 464 | unzip_bot=unzip_bot, c_id=c_id, doc_f=doc_f, source=source, message=message 465 | ) 466 | except FileNotFoundError: 467 | await unzip_bot.send_message( 468 | chat_id=Config.LOGS_CHANNEL, 469 | text=messages.get(file="up_helper", key="ARCHIVE_GONE", user_id=c_id), 470 | ) 471 | except BaseException: 472 | pass 473 | 474 | 475 | async def merge_split_archives(user_id, path): 476 | cmd = f'cd "{path}" && cat * > MERGED_{user_id}.zip' 477 | await run_shell_cmds(cmd) 478 | 479 | 480 | # Function to remove basic markdown characters from a string 481 | async def rm_mark_chars(text: str): 482 | return re.sub(pattern="[*`_]", repl="", string=text) 483 | 484 | 485 | # Function to answer queries 486 | async def answer_query( 487 | query, message_text: str, answer_only: bool = False, unzip_client=None, buttons=None 488 | ): 489 | try: 490 | if answer_only: 491 | await query.answer(await rm_mark_chars(message_text), show_alert=True) 492 | else: 493 | await query.message.edit(message_text, reply_markup=buttons) 494 | except: 495 | try: 496 | if unzip_client: 497 | await unzip_client.send_message( 498 | chat_id=query.message.chat.id, 499 | text=message_text, 500 | reply_markup=buttons, 501 | ) 502 | else: 503 | await unzipbot_client.send_message( 504 | chat_id=query.message.chat.id, 505 | text=message_text, 506 | reply_markup=buttons, 507 | ) 508 | except: 509 | pass 510 | --------------------------------------------------------------------------------