Repository: ranjan-mohanty/vfs-appointment-bot Branch: main Commit: 87c4e739345b Files: 33 Total size: 80.9 KB Directory structure: gitextract_xbbv28z9/ ├── .flake8 ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── dependabot.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── build.yml │ ├── codeql.yml │ ├── dependency-review.yml │ ├── publish-testpypi.yml │ ├── publish.yml │ └── scorecard.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── pyproject.toml └── vfs_appointment_bot/ ├── main.py ├── notification/ │ ├── email_client.py │ ├── notification_client.py │ ├── notification_client_factory.py │ ├── telegram_client.py │ └── twilio_client.py ├── utils/ │ ├── config_reader.py │ ├── date_utils.py │ └── timer.py └── vfs_bot/ ├── vfs_bot.py ├── vfs_bot_de.py ├── vfs_bot_factory.py └── vfs_bot_it.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .flake8 ================================================ [flake8] exclude = .git, .github, __pycache__, venv, dist, config, build max-complexity = 10 max-line-length = 120 ================================================ FILE: .github/FUNDING.yml ================================================ github: ranjan-mohanty ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: "" labels: "" assignees: "" --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: "" labels: "" assignees: "" --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" ignore: - dependency-name: "*" update-types: ["version-update:semver-patch"] - package-ecosystem: github-actions directory: / schedule: interval: daily ignore: - dependency-name: "*" update-types: ["version-update:semver-patch"] ================================================ FILE: .github/pull_request_template.md ================================================ ## Pull Request Template **Thank you for contributing to the VFS appointment bot project!** To streamline the review process, please fill out the details below when creating a pull request. **Title:** - Briefly describe your changes here **Description:** - What changes did you make? - Explain the purpose of your changes and how they address an issue or improve the scraper. - If applicable, mention any new features you've implemented. - Include steps to reproduce any bug fixes (if applicable). - How did you test your changes? - Did you add new unit tests? - Did you manually test with different scenarios? **Additional Information:** - Did you add any dependencies or external libraries? - Did you modify the documentation? If so, how? **Checklist:** - [ ] I followed the Coding Style Guide: [https://peps.python.org/pep-0008/](https://peps.python.org/pep-0008/). - [ ] I added clear and concise comments to my code. - [ ] I added unit tests for new features or bug fixes (if applicable). - [ ] I ensured my changes do not introduce regressions. **Optional:** - Issue reference (if related to an existing issue): # (issue number) **By providing these details, you'll help us review your pull request efficiently. We appreciate your contributions!** ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: workflow_call: push: branches: - "**" tags-ignore: - "**" permissions: contents: read jobs: build: name: Build distribution runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 with: egress-policy: audit - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Python uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: "3.x" - name: Install pypa/build run: python3 -m pip install build --user - name: Build a binary wheel and a source tarball run: python3 -m build - name: Store the distribution packages uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: python-package-distributions path: dist/ ================================================ FILE: .github/workflows/codeql.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: ["main"] pull_request: # The branches below must be a subset of the branches above branches: ["main"] schedule: - cron: "0 0 * * 1" permissions: contents: read jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: ["python"] # CodeQL supports [ $supported-codeql-languages ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Harden Runner uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 with: category: "/language:${{matrix.language}}" ================================================ FILE: .github/workflows/dependency-review.yml ================================================ # Dependency Review Action # # This Action will scan dependency manifest files that change as part of a Pull Request, # surfacing known-vulnerable versions of the packages declared or updated in the PR. # Once installed, if the workflow run is marked as required, # PRs introducing known-vulnerable packages will be blocked from merging. # # Source repository: https://github.com/actions/dependency-review-action name: 'Dependency Review' on: [pull_request] permissions: contents: read jobs: dependency-review: runs-on: ubuntu-latest steps: - name: Harden Runner uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 with: egress-policy: audit - name: 'Checkout Repository' uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: 'Dependency Review' uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 ================================================ FILE: .github/workflows/publish-testpypi.yml ================================================ name: Publish - TestPyPI on: push: tags: - '**' permissions: contents: read jobs: build: uses: ./.github/workflows/build.yml publish-to-testpypi: name: Publish to TestPyPI needs: - build runs-on: ubuntu-latest environment: name: testpypi url: https://test.pypi.org/p/vfs-appointment-bot permissions: id-token: write steps: - name: Harden Runner uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 with: egress-policy: audit - name: Download all the dists uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: python-package-distributions path: dist/ - name: Publish distribution to TestPyPI uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # release/v1 with: repository-url: https://test.pypi.org/legacy/ github-release: if: ${{contains(github.ref, 'rc')}} name: Release needs: - publish-to-testpypi runs-on: ubuntu-latest permissions: contents: write id-token: write steps: - name: Harden Runner uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 with: egress-policy: audit - name: Download all the dists uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: python-package-distributions path: dist/ - name: Sign the dists with Sigstore uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0 with: inputs: >- ./dist/*.tar.gz ./dist/*.whl - name: Create GitHub Release env: GITHUB_TOKEN: ${{ github.token }} run: >- gh release create '${{ github.ref_name }}' --repo '${{ github.repository }}' --prerelease - name: Upload artifact signatures to GitHub Release env: GITHUB_TOKEN: ${{ github.token }} run: >- gh release upload '${{ github.ref_name }}' dist/** --repo '${{ github.repository }}' ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish - PyPI on: push: tags-ignore: - '*rc*' permissions: contents: read jobs: build: uses: ./.github/workflows/build.yml publish-to-pypi: name: Publish to PyPI needs: - build runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/vfs-appointment-bot permissions: id-token: write steps: - name: Harden Runner uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 with: egress-policy: audit - name: Download all the dists uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: python-package-distributions path: dist/ - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # release/v1 github-release: name: Release needs: - publish-to-pypi runs-on: ubuntu-latest permissions: contents: write id-token: write steps: - name: Harden Runner uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 with: egress-policy: audit - name: Download all the dists uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: python-package-distributions path: dist/ - name: Sign the dists with Sigstore uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0 with: inputs: >- ./dist/*.tar.gz ./dist/*.whl - name: Create GitHub Release env: GITHUB_TOKEN: ${{ github.token }} run: >- gh release create '${{ github.ref_name }}' --repo '${{ github.repository }}' - name: Upload artifact signatures to GitHub Release env: GITHUB_TOKEN: ${{ github.token }} run: >- gh release upload '${{ github.ref_name }}' dist/** --repo '${{ github.repository }}' ================================================ FILE: .github/workflows/scorecard.yml ================================================ # This workflow uses actions that are not certified by GitHub. They are provided # by a third-party and are governed by separate terms of service, privacy # policy, and support documentation. name: Scorecard Security on: push: branches: ["main"] # Declare default permissions as read only. permissions: read-all jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. security-events: write # Needed to publish results and get a badge (see publish_results below). id-token: write # Uncomment the permissions below if installing in a private repository. # contents: read # actions: read steps: - name: Harden Runner uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 with: egress-policy: audit - name: "Checkout code" uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 with: results_file: results.sarif results_format: sarif # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: # - you want to enable the Branch-Protection check on a *public* repository, or # - you are installing Scorecard on a *private* repository # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. # repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. # For private repositories: # - `publish_results` will always be set to `false`, regardless # of the value entered here. publish_results: true # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 with: sarif_file: results.sarif ================================================ FILE: .gitignore ================================================ # Distribution / packaging dist/ eggs/ *.egg-info/ .installed.cfg *.egg MANIFEST poetry.lock # Unit test / coverage reports .coverage .coverage.* .pytest_cache/ # Translations *.mo *.pot # IDEs/Build Tools (consider including if you use them) .idea/ .vscode/ build/ # Virtual Environments .venv/ env/ venv # Logs app.log # Local Config config.ini *.ini ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/gitleaks/gitleaks rev: v8.16.3 hooks: - id: gitleaks - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/pylint-dev/pylint rev: v2.17.2 hooks: - id: pylint - repo: https://github.com/pycqa/flake8 rev: ^4.0.1 hooks: - id: flake8 - repo: https://github.com/psf/black rev: ^22.3.2 hooks: - id: black ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at . All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, available [here](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). For answers to common questions about this code of conduct, see the FAQ [here](https://www.contributor-covenant.org/faq). Translations are available [here](https://www.contributor-covenant.org/translations). ================================================ FILE: CONTRIBUTING.md ================================================ ## Welcome to Amazon Product Details Scraper contributing guide Thank you for investing your time in contributing to our project! We welcome contributions to this project! Here's how you can get involved: **1. Issues:** - **Bug reports:** If you encounter a bug, please search existing issues first to avoid duplicates. If you can't find a relevant issue, create a new one with a clear description of the problem, including steps to reproduce it. - **Feature requests:** If you have an idea for a new feature, feel free to create an issue to discuss it. Explain the functionality you'd like to see and how it would benefit the project. **2. Pull Requests:** - **Fork the repository:** Create a fork of the repository on GitHub. - **Clone your fork:** Clone your forked repository to your local machine. - **Create a new branch:** Create a new branch for your changes (e.g., `feature/your_feature_name`). - **Implement your changes:** Make your modifications to the code. - **Write clear commit messages:** Use descriptive commit messages that explain your changes. - **Test your changes:** Ensure your changes don't introduce regressions. Consider adding unit tests if applicable. - **Push to your branch:** Push your changes to your branch on your forked repository. - **Create a pull request:** Create a pull request from your branch to the main branch of the upstream repository. - **Address feedback:** We will review your pull request and provide feedback. Be prepared to address any comments or suggestions. **3. Coding Style:** - Follow [PEP 8 style guide](https://peps.python.org/pep-0008/) for Python code. - Use consistent formatting and indentation. **4. License:** - This project is licensed under the MIT License. Please ensure your contributions are compatible with the license. **5. Code Documentation:** - Add comments to document your code, especially complex sections. - Consider using docstrings to explain functions and classes. **Thank you for your contributions!** **Additional Notes:** - You are not obligated to include all the sections mentioned above. Adapt them to your project's specific needs. - Feel free to add a section about setting up development environment if your project has dependencies. - Consider including a code of conduct to promote a positive and inclusive community. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Ranjan Mohanty Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # VFS Appointment Bot [![GitHub License](https://img.shields.io/github/license/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/blob/main/LICENSE) [![GitHub Release](https://img.shields.io/github/v/release/ranjan-mohanty/vfs-appointment-bot?logo=GitHub)](https://github.com/ranjan-mohanty/vfs-appointment-bot/releases) [![PyPI - Version](https://img.shields.io/pypi/v/vfs-appointment-bot?logo=pypi)](https://pypi.org/project/vfs-appointment-bot) [![Downloads](https://static.pepy.tech/badge/vfs-appointment-bot)](https://pepy.tech/project/vfs-appointment-bot) [![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fhits.dwyl.com%2Franjan-mohanty%2Fvfs-appointment-bot.json&style=flat&logo=GitHub&label=views)](https://github.com/ranjan-mohanty/vfs-appointment-bot) [![GitHub forks](https://img.shields.io/github/forks/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/forks) [![GitHub Repo stars](https://img.shields.io/github/stars/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/stargazers) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ranjan-mohanty/vfs-appointment-bot/build.yml)](https://github.com/ranjan-mohanty/vfs-appointment-bot/actions/workflows/build.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/21f1ecd428ec4342980020a6ef383439)](https://app.codacy.com/gh/ranjan-mohanty/vfs-appointment-bot/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ranjan-mohanty/vfs-appointment-bot/badge)](https://securityscorecards.dev/viewer/?uri=github.com/ranjan-mohanty/vfs-appointment-bot) [![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/ranjan-mohanty/vfs-appointment-bot)](https://github.com/ranjan-mohanty/vfs-appointment-bot/issues) ![Libraries.io dependency status for GitHub repo](https://img.shields.io/librariesio/github/ranjan-mohanty/vfs-appointment-bot) [![Twitter](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Fgithub.com%2Franjan-mohanty%2Fvfs-appointment-bot)](https://twitter.com/intent/tweet?text=Check%20this%20out%20&url=https%3A%2F%2Fgithub.com%2Franjan-mohanty%2Fvfs-appointment-bot) This Python script(**vfs-appointment-bot**) automates checking for appointments at VFS Global portal in a specified country. ## Installation The `vfs-appointment-bot` script can be installed using two methods: ### 1. Using pip It is the preferred method for installing `vfs-appointment-bot`. Here's how to do it: 1. **Create a virtual environment (Recommended):** ```bash python3 -m venv venv ``` This creates a virtual environment named `venv` to isolate project dependencies from your system-wide Python installation (**recommended**). 2. **Activate the virtual environment:** **Linux/macOS:** ```bash source venv/bin/activate ``` **Windows:** ```bash venv\Scripts\activate ``` 3. **Install using pip:** ```bash pip install vfs-appointment-bot ``` This will download and install the `vfs-appointment-bot` package and its dependencies into your Python environment. ### 2. Manual Installation For an alternative installation method, clone the source code from the project repository and install it manually. 1. **Clone the repository:** ```bash git clone https://github.com/ranjan-mohanty/vfs-appointment-bot ``` 2. **Navigate to the project directory:** ```bash cd vfs-appointment-bot ``` 3. **Create a virtual environment (Recommended):** ```bash python3 -m venv venv ``` This creates a virtual environment named `venv` to isolate project dependencies from your system-wide Python installation (**recommended**). 4. **Activate the virtual environment:** **Linux/macOS:** ```bash source venv/bin/activate ``` **Windows:** ```bash venv\Scripts\activate ``` 5. **Install dependencies:** ```bash pip install poetry poetry install ``` 6. **Install playwright dependencies:** ```bash playwright install ``` ## Configuration 1. Download the [`config/config.ini`](https://raw.githubusercontent.com/ranjan-mohanty/vfs-appointment-bot/main/config/config.ini) template. ```bash curl -L https://raw.githubusercontent.com/ranjan-mohanty/vfs-appointment-bot/main/config/config.ini -o config.ini ``` 2. Update the vfs credentials and notification channel preferences. See the [Notification Channels](#notification-channels) section for details on configuring email, Twilio, and Telegram notifications. 3. Export the path of the config file to the environment variable `VFS_BOT_CONFIG_PATH` ```bash export VFS_BOT_CONFIG_PATH=/config.ini ``` **If you installed the script by cloning the repository (manual installation)**, you can directly edit the values in `config/config.ini`. ## Usage 1. **Command-Line Argument:** The script requires the source and destination country code ([as per ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements)) to be provided as a command-line argument using the `-sc` or `--source-country-code` and `-dc` or `--destination-country-code` option. 2. **Running the Script:** There are two ways to provide required appointment details: - **Responding to User Prompts (recommended):** ```bash vfs-appointment-bot -sc IN -dc DE ``` The script will prompt you to enter the required apponitment parameters for the specified country. - **Using `-ap` or `--appointment-params`:** Specify appointment details in a comma-separated (**not space-separated**) key-value format: ```bash vfs-appointment-bot -sc IN -dc DE -ap visa_center=X,visa_category=Y,visa_sub_category=Z ``` The script will then connect to the VFS Global website for the specified country, search for available appointments using the provided or entered parameters, and potentially send notifications (depending on your configuration). ## Notification Channels It currently supports three notification channels to keep you informed about appointment availability: - **Email:** Sends notifications via a Gmail account. - **Twilio (SMS & Voice Call):** Enables alerts through text messages and phone calls using Twilio's services. - **Telegram:** Sends notifications directly to your Telegram account through a bot. **Configuring Notifications:** **Email:** 1. **Email Account:** You'll need a **Gmail account** for sending notifications. 2. **App Password:** Generate an app password for your Gmail account instead of your regular password. Refer to Google's guide for generating app passwords: [https://support.google.com/accounts/answer/185833?hl=en](https://support.google.com/accounts/answer/185833?hl=en). 3. **Configuration File:** Update your application's configuration file (`config.ini`) with the following details: - **`email` (Required):** Your Gmail address. - **`password` (Required):** Your generated Gmail app password. **Twilio:** 1. **Create a Twilio Account (if needed):** Sign up for a free Twilio account at [https://www.twilio.com/en-us](https://www.twilio.com/en-us) to obtain account credentials and phone numbers. 2. **Retrieve Credentials:** Locate your account SID, auth token, and phone numbers within your Twilio account dashboard. 3. **Configuration File:** Update your application's configuration file (`config.ini`) with: - `auth_token` (Required): Your Twilio auth token - `account_sid` (Required): Your Twilio account SID - `sms_enabled` (Optional): Enables SMS notifications (default: True) - `call_enabled` (Optional): Enables voice call notifications (default: False) - `url` (Optional): Twilio API URL (Only needed if call is enabled) - `to_num` (Required): Recipient phone number for notifications - `from_num` (Required): Twilio phone number you'll use for sending messages **Telegram:** 1. **Create a Telegram Bot:** Visit [https://telegram.me/BotFather](https://telegram.me/BotFather) to create a Telegram bot. Follow the on-screen instructions to obtain your bot's token. 2. **Configuration File:** Update your application's configuration file (`config.ini`) with: - **`bot_token` (Required):** Your Telegram bot token obtained from BotFather. - **`chat_id` (Optional):** The specific Telegram chat ID where you want to receive notifications. If omitted, the bot will send notifications to the chat where it was messaged from. To find your chat ID, you can create a group chat with just yourself and then use the `/my_id` command within the bot. ## Supported Countries and Appointment Parameters The following table lists currently supported countries and their corresponding appointment parameters: | Country | Appointment Parameters | | -------------------------- | ----------------------------------------------------------- | | India(IN) - Germany(DE) | visa_category, visa_sub_category, visa_center | | Iraq(IQ) - Germany(DE) | visa_category, visa_sub_category, visa_center | | Morocco(MA) - Italy(IT) | visa_category, visa_sub_category, visa_center, payment_mode | | Azerbaijan(AZ) - Italy(IT) | visa_category, visa_sub_category, visa_center | **Notes:** - Appointment parameters might vary depending on the specific country and visa type. Always consult VFS Global's website for the latest information. ## Known Issues **1. Login Failures After Frequent Requests:** If the bot makes login requests to the VFS website too frequently, the VFS system might temporarily block your access due to suspected automation. This can lead to login failures. - **Workaround:** - **Reduce request frequency:** Consider increasing the delay between bot runs to avoid triggering VFS's blocking mechanisms. You can adjust the interval in the configuration or code. - **Retry after 2 hours:** If you encounter a login failure, wait for at least 2 hours before retrying. The VFS block should expire within this timeframe. **2. Occasional Captcha Verification:** The VFS website requires a CAPTCHA verification step during login. Currently, the bot does not have a built-in CAPTCHA solver. - **Workaround:** - **Wait and Retry:** Sometimes, CAPTCHAs appear due to temporary website issues. Wait for a while and try again later. - **Retry in another browser:** CAPTCHAs are often solved automatically in the Firefox browser. If it still fails, retry the login process in another browser by setting `browser_type` to `"chromium" or "webkit"` in your `config.ini` file. **Note:** We are constantly working to improve the bot's functionality. Future updates might include integrated CAPTCHA solving capabilities. ## Extending Country Support This script is currently designed to work with the VFS Global website for Germany. It might be possible to extend support for other countries by modifying the script to handle potential variations in website structure and parameter requirements across different VFS Global country pages. ## Contributing We welcome contributions from the community to improve this project! Here's how you can get involved: - **Report issues:** If you encounter any bugs or problems with the script, please create an issue on the project's repository. - **Suggest improvements:** Do you have ideas for making the script more user-friendly or feature-rich? Feel free to create an issue or pull request on the repository. - **Submit pull requests:** If you've made code changes that you think would benefit the project, create a pull request on the repository. Please follow any contribution guidelines outlined in a CONTRIBUTING.md file. ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=ranjan-mohanty/vfs-appointment-bot&type=Date)](https://star-history.com/#ranjan-mohanty/vfs-appointment-bot&Date) ## Disclaimer This script is provided as-is and is not affiliated with VFS Global. It's your responsibility to ensure you're complying with VFS Global's terms and conditions when using this script. Be aware that website structures and appointment availability mechanisms might change over time. ================================================ FILE: SECURITY.md ================================================ ## Security Policy for VFS Appointment Bot This document outlines the security policy for the VFS Appointment Bot project. **1. Reporting Vulnerabilities:** We appreciate your help in keeping this project secure. If you discover a security vulnerability, please report it responsibly by following these steps: **1.1 Public Reporting:** - If the vulnerability can be disclosed publicly without compromising security, you can create a public issue report on the project's GitHub repository. **1.2 Private Reporting:** - **We have enabled private vulnerability reporting on GitHub.** For vulnerabilities that should be kept confidential until a fix is released, please follow the steps outlined in the [GitHub documentation](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability) - **Detailed Description:** Provide a detailed description of the vulnerability, including steps to reproduce it and potential impact. - **Confidentiality:** Keep the vulnerability confidential until a fix is released to prevent exploitation. We will acknowledge your report and work on a fix with the following goals: - **Timely Response:** We will address reported vulnerabilities as quickly as possible. - **Transparency:** We will keep you informed of the progress towards a fix and its estimated release date. - **Fix Release:** We will release a fix for the vulnerability in a timely manner. **2. Secure Coding Practices:** The script development follows best practices for secure coding to minimize vulnerabilities. These practices include: - **Input Validation:** User input is sanitized to prevent injection attacks (e.g., SQL injection, XSS). - **Dependency Management:** Dependencies are kept up-to-date to address known vulnerabilities in external libraries. - **Secret Handling:** Sensitive information (if any) is not stored in plain text. **3. Supported Versions:** We will only provide security fixes for the most recent versions of the bot. Users are encouraged to stay up-to-date with the latest releases to benefit from the latest security improvements. **4. Disclaimer:** While we strive to maintain the security of this script through development practices, it's provided as-is and we cannot guarantee that it is completely free of vulnerabilities. Users are encouraged to exercise caution when using any automated tools that interact with websites. **5. Responsible Use:** This script is intended for automating appointment checks on a public website. Users are responsible for using the script in a compliant and ethical manner, respecting robots.txt and terms of service of VFS Global's website. **6. Reporting Abuses:** If you suspect any misuse of this script for malicious purposes, please contact the project maintainer immediately. We appreciate your cooperation in using this script responsibly! ================================================ FILE: pyproject.toml ================================================ # Project metadata [tool.poetry] name = "vfs-appointment-bot" version = "1.1.1" description = "VFS Appointment Bot - This script automates checking for appointments at VFS Global offices in a specified country." authors = ["Ranjan Mohanty "] license = "MIT" readme = "README.md" keywords = ["vfs", "vfs-bot", "vfs-appointment-bot", "visa-appointment-bot"] # URLs [tool.poetry.urls] repository = "https://github.com/ranjan-mohanty/vfs-appointment-bot/blob/main/README.md" homepage = "https://github.com/ranjan-mohanty/vfs-appointment-bot/blob/main" # Build System [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" # Dependencies [tool.poetry.dependencies] python = "^3.9" playwright = "^1.43.0" playwright-stealth = "^1.0.6" twilio = "^9.0.4" tqdm = "^4.66.2" [tool.poetry.dev-dependencies] flake8 = "^7.0.0" black = "^24.4.2" #Entry points [tool.poetry.scripts] vfs-appointment-bot = "vfs_appointment_bot.main:main" ================================================ FILE: vfs_appointment_bot/main.py ================================================ import argparse import logging import sys from typing import Dict from vfs_appointment_bot.utils.config_reader import get_config_value, initialize_config from vfs_appointment_bot.utils.timer import countdown from vfs_appointment_bot.vfs_bot.vfs_bot import LoginError from vfs_appointment_bot.vfs_bot.vfs_bot_factory import ( UnsupportedCountryError, get_vfs_bot, ) class KeyValueAction(argparse.Action): """Custom action class for parsing appointment parameters. This class handles parsing comma-separated key-value pairs provided through the `--appointment-params` argument. It ensures the format is valid (key=value) and stores the parsed parameters as a dictionary. """ def __call__(self, parser, namespace, values, option_string=None): try: appointment_params: Dict[str, str] = { key.strip(): value.strip() for key, value in (item.split("=") for item in values.split(",")) } setattr(namespace, "appointment_params", appointment_params) except ValueError: parser.error( f"Invalid value format for {option_string}, use key=value pairs" ) def main() -> None: """ Entry point for the VFS Appointment Bot. This function sets up logging, parses command-line arguments, and runs the VFS appointment checking process in a continuous loop. It catches exceptions for unsupported countries and unexpected errors, logging them appropriately. Raises: UnsupportedCountryError: If the provided country code is not supported by the bot. Exception: For any other unexpected errors encountered during execution. """ initialize_logger() initialize_config() parser = argparse.ArgumentParser( description="VFS Appointment Bot: Checks for appointments at VFS Global" ) required_args = parser.add_argument_group("required arguments") required_args.add_argument( "-sc", "--source-country-code", type=str, help="The ISO 3166-1 alpha-2 source country code (refer to README)", metavar="", required=True, ) required_args.add_argument( "-dc", "--destination-country-code", type=str, help="The ISO 3166-1 alpha-2 destination country code (refer to README)", metavar="", required=True, ) parser.add_argument( "-ap", "--appointment-params", type=str, default=None, help="Comma-separated key-value pairs for additional appointment details (refer to VFS website)", action=KeyValueAction, metavar="", ) args = parser.parse_args() source_country_code = args.source_country_code destination_country_code = args.destination_country_code try: while True: vfs_bot = get_vfs_bot(source_country_code, destination_country_code) appointment_found = vfs_bot.run(args) if appointment_found: break countdown( int(get_config_value("default", "interval")), "Next appointment check in", ) except (UnsupportedCountryError, LoginError) as e: logging.error(e) except Exception as e: logging.exception(e) def initialize_logger(): file_handler = logging.FileHandler("app.log", mode="a") file_handler.setFormatter( logging.Formatter( "[%(asctime)s] %(levelname)s [%(filename)s:%(lineno)d] %(message)s" ) ) stream_handler = logging.StreamHandler(sys.stdout) stream_handler.setFormatter(logging.Formatter("[%(asctime)s] %(message)s")) logging.basicConfig( level=logging.INFO, format="[%(asctime)s] %(levelname)s [%(filename)s:%(lineno)d] %(message)s", handlers=[ file_handler, stream_handler, ], ) if __name__ == "__main__": main() ================================================ FILE: vfs_appointment_bot/notification/email_client.py ================================================ import smtplib import logging from vfs_appointment_bot.notification.notification_client import NotificationClient class EmailClient(NotificationClient): def __init__(self): """ Initializes the email client with configuration data. This constructor retrieves configuration settings from the designated section (e.g., `"email"`) of the application configuration and validates them using the base class validation logic. """ required_keys = ["email", "password"] super().__init__("email", required_keys) def send_notification(self, message: str) -> None: """ Sends a notification message through the email channel. This method sends an email notification using the provided message content. It connects securely to the configured SMTP server (e.g., Gmail's SMTP), authenticates with the provided credentials, and constructs a well-formatted email before sending it. Args: message (str): The message content to be included in the email. """ email: str = self.config.get("email") password: str = self.config.get("password") email_text = self.__construct_email_text(email, message) smtp_server = smtplib.SMTP_SSL("smtp.gmail.com", 465) smtp_server.ehlo() smtp_server.login(email, password) smtp_server.sendmail(email, email, email_text) smtp_server.close() logging.info("Email sent successfully!") def __construct_email_text(self, email: str, message: str) -> str: """ Constructs a formatted email text with sender, receiver, subject, and message body. Args: message (str): The message content to be included in the email body. Returns: str: The formatted email text ready for sending. """ return f"From: {email}\nTo: {email}\nSubject: VFS Appointment Bot Notification\n\n{message}" ================================================ FILE: vfs_appointment_bot/notification/notification_client.py ================================================ from abc import ABC, abstractmethod from typing import List from vfs_appointment_bot.utils.config_reader import get_config_section class NotificationClient(ABC): """Abstract base class for notification clients. This class defines the common interface for notification clients used throughout the application. Subclasses must implement the `send_notification` method to provide specific notification sending functionality for their respective channels. """ def __init__(self, config_section: str, required_config_keys: List[str]): """ Initializes the client with configuration data. Args: config_section (str): The name of the configuration section containing client-specific settings. required_config_keys (list[str]): A list of required keys that must be present in the configuration section. """ self.required_keys = required_config_keys self.config = get_config_section(config_section) self._validate_config(required_config_keys) @abstractmethod def send_notification(self, message: str) -> None: """ Sends a notification message to the recipient. This method is abstract and must be implemented by subclasses to provide the specific logic for sending notifications through their respective channels. Args: message (str): The message content to be sent. """ def _validate_config(self, required_config_keys: list[str]): """ Validates the configuration of the notification client. This method checks if all required configuration keys are present and have non-null values. If any validation errors are found, appropriate exceptions are raised. Args: required_config_keys (list[str]): A list of required keys that must be present in the configuration section. """ missing_keys = required_config_keys - self.config.keys() if missing_keys: raise NotificationClientConfigValidationError( f"Missing required configuration keys: {', '.join(missing_keys)}" ) for key in self.required_keys: if self.config.get(key) is None: raise NotificationClientConfigValidationError( f"Value for key '{key}' cannot be null." ) class NotificationClientConfigValidationError(Exception): """Exception raised when notification client configuration validation fails.""" class NotificationClientError(Exception): """Exception raised when an error occurs during notification sending.""" ================================================ FILE: vfs_appointment_bot/notification/notification_client_factory.py ================================================ from vfs_appointment_bot.notification.notification_client import NotificationClient class UnsupportedNotificationChannelError(Exception): """Raised when an unsupported notification channel is provided.""" def get_notification_client(channel: str) -> NotificationClient: """Retrieves the appropriate notification client for a given channel. This function creates an instance of a notification client class based on the provided channel string. Currently supported channels include "telegram" and "slack". If an unsupported channel is provided, a `ValueError` exception is raised. Args: channel (str): The notification channel name. Returns: NotificationClient: An instance of the `NotificationClient` sub class specific to the provided channel. Raises: UnsupportedNotificationChannelError: If the provided notification channel is not supported. """ if channel == "telegram": from .telegram_client import TelegramClient return TelegramClient() elif channel == "slack": from .twilio_client import TwilioClient return TwilioClient() elif channel == "email": from .email_client import EmailClient return EmailClient() else: raise UnsupportedNotificationChannelError( f"Notification channel '{channel}' is not supported" ) ================================================ FILE: vfs_appointment_bot/notification/telegram_client.py ================================================ import logging import requests from vfs_appointment_bot.notification.notification_client import NotificationClient class TelegramClient(NotificationClient): """Concrete implementation of NotificationClient for the Telegram channel. This class provides functionality for sending notifications through the Telegram messaging platform. It inherits from the abstract `NotificationClient` class and implements the required `send_notification` method for Telegram-specific notification sending logic. """ def __init__(self): """ Initializes the Telegram client with configuration data. This constructor retrieves configuration settings from the "telegram" section of the application configuration and validates them using the base class validation logic. """ required_keys = ["bot_token", "chat_id", "parse_mode"] super().__init__("telegram", required_keys) def send_notification(self, message: str) -> None: """ Sends a notification message through the Telegram channel. This method constructs a Telegram API request URL using the retrieved configuration settings (bot token, chat ID, and parse mode) and sends a GET request to the Telegram API with the message content. The response from the Telegram API is logged for debugging purposes. Args: message (str): The message content to be sent as a Telegram notification. """ bot_token: str = self.config.get("bot_token") chat_id: str = self.config.get("chat_id") parse_mode: str = self.config.get("parse_mode") url = ( f"https://api.telegram.org/bot{bot_token}/sendMessage?" + f"chat_id={chat_id}&parse_mode={parse_mode}&text={message}" ) requests.get(url, timeout=3000).json() logging.info("Telegram message sent successfully!") ================================================ FILE: vfs_appointment_bot/notification/twilio_client.py ================================================ import logging from typing import Optional from twilio.rest import Client from vfs_appointment_bot.notification.notification_client import NotificationClient class TwilioClient(NotificationClient): """Concrete implementation of NotificationClient for the Twilio channel. This class provides functionality for sending notifications through the Twilio communication platform. It inherits from the abstract `NotificationClient` class and implements the required `send_notification` method for Twilio-specific notification sending logic, including SMS messages and calls (if enabled). """ def __init__(self): """ Initializes the Twilio client with configuration data. This constructor retrieves configuration settings from the "twilio" section of the application configuration and validates them using the base class validation logic. """ required_config_keys = [ "to_num", "from_num", "account_sid", "auth_token", "url", "call_enabled", ] super().__init__("twilio", required_config_keys) def send_notification(self, message: str) -> None: """ Sends a notification message through the Twilio channel. This method sends an SMS message using the provided message content. Optionally, if the "call_enabled" flag is set to True in the configuration, it also initiates a call to the specified phone number using a pre-recorded URL (provided by the "url" configuration option). Args: message (str): The message content to be sent as a Twilio SMS. """ url: Optional[str] = self.config.get("url") auth_token: str = self.config.get("auth_token") account_sid: str = self.config.get("account_sid") to_num: str = self.config.get("to_num") from_num: str = self.config.get("from_num") call_enabled: bool = self.config.get("call_enabled", False) self.__send_message(message, auth_token, account_sid, to_num, from_num) if call_enabled: self.__call(url, auth_token, account_sid, to_num, from_num) def __send_message( self, message: str, auth_token: str, account_sid: str, to_num: str, from_num: str, ) -> None: """ Sends an SMS message using the Twilio API. This private helper method creates a Twilio client instance and uses it to send an SMS message with the provided content to the specified recipient phone number. Args: message (str): The message content to be sent. auth_token (str): The Twilio account authentication token. account_sid (str): The Twilio account SID. to_num (str): The recipient phone number. from_num (str): The Twilio phone number used to send the message. """ client = Client(account_sid, auth_token) client.messages.create(to=to_num, from_=from_num, body=message) logging.info("Message sent successfully!") def __call( self, url: Optional[str], auth_token: str, account_sid: str, to_num: str, from_num: str, ) -> None: """ Initiates a call using the Twilio API (if URL is provided). This private helper method creates a Twilio client instance and uses it to initiate a call to the specified recipient phone number, using a pre-recorded URL for the call content (if provided in the configuration). Args: url (Optional[str]): The URL for the pre-recorded call content. auth_token (str): The Twilio account authentication token. account_sid (str): The Twilio account SID. to_num (str): The recipient phone number. from_num (str): The Twilio phone number used to initiate the call. """ if url: client = Client(account_sid, auth_token) client.calls.create(from_=from_num, to=to_num, url=url) logging.info("Call request sent successfully!") else: logging.warning("No URL provided for call request!") ================================================ FILE: vfs_appointment_bot/utils/config_reader.py ================================================ import os from configparser import ConfigParser from typing import Dict _config: ConfigParser = None def initialize_config(config_dir="config"): """ Reads all INI configuration files in a directory and caches the result. Also reads user config from `VFS_BOT_CONFIG_PATH` env var (if set) Args: config_dir: The directory containing configuration files (default: "config"). """ global _config if not _config: _config = ConfigParser() for entry in os.scandir(config_dir): if entry.is_file() and entry.name.endswith(".ini"): config_file_path = os.path.join(config_dir, entry.name) _config.read(config_file_path) # Read user defined config file user_config_path = os.environ.get("VFS_BOT_CONFIG_PATH") if user_config_path: _config.read(user_config_path) def get_config_section(section: str, default: Dict = None) -> Dict: """ Get a configuration section as a dictionary. Args: section: The name of the section to retrieve. default: A dictionary containing default values for the section (optional). Returns: A dictionary containing the configuration for the specified section, or the provided default dictionary if the section is not found. """ if _config.has_section(section): return dict(_config[section]) else: return default or {} def get_config_value(section: str, key: str, default: str = None) -> str: """ Get a specific configuration value. Args: section: The name of the section containing the value. key: The name of the key to retrieve. default: The default value to return if the section or key is not found (optional). Returns: The value associated with the given key within the specified section, or the provided default value if the section or key does not exist. """ if _config.has_section(section) and _config.has_option(section, key): return _config[section][key] else: return default ================================================ FILE: vfs_appointment_bot/utils/date_utils.py ================================================ import re def extract_date_from_string(text): pattern = r"(\d{4}-\d{2}-\d{2}|\d{2}-\d{2}-\d{4}|\d{2}-\d{2}-\d{2})" match = re.search(pattern, text) if match: return match.group() else: return None ================================================ FILE: vfs_appointment_bot/utils/timer.py ================================================ import time from tqdm import tqdm def countdown(t: int, message="Countdown", unit="seconds"): """ Implements a countdown timer Args: t (int): The initial countdown time in the specified unit (e.g., seconds, minutes). message (str, optional): The message to display during the countdown. Defaults to "Countdown". unit (str, optional): The unit of time for the countdown. Defaults to "seconds". """ with tqdm( total=t, desc=message, unit=unit, bar_format="{desc}: {remaining}" ) as pbar: for _ in range(t): time.sleep(1) pbar.update(1) ================================================ FILE: vfs_appointment_bot/vfs_bot/vfs_bot.py ================================================ import argparse import logging from abc import ABC, abstractmethod from typing import Dict, List import playwright from playwright.sync_api import sync_playwright from playwright_stealth import stealth_sync from vfs_appointment_bot.utils.config_reader import get_config_value from vfs_appointment_bot.notification.notification_client_factory import ( get_notification_client, ) class LoginError(Exception): """Exception raised when login fails.""" class VfsBot(ABC): """ Abstract base class for VfsBot Provides common functionalities like login, pre-login steps, appointment checking, and notification. Subclasses are responsible for implementing country-specific login and appointment checking logic. """ def __init__(self): """ Initializes a VfsBot instance for a specific country. """ self.source_country_code = None self.destination_country_code = None self.appointment_param_keys: List[str] = [] def run(self, args: argparse.Namespace = None) -> bool: """ Starts the VFS bot for appointment checking and notification. This method reads configuration values, performs login, checks for appointments based on provided arguments, and sends notifications if appointments are found. Args: args (argparse.Namespace, optional): Namespace object containing parsed command-line arguments. Defaults to None. Returns: bool: True if appointments were found, False otherwise. """ logging.info( f"Starting VFS Bot for {self.source_country_code.upper()}-{self.destination_country_code.upper()}" ) # Configuration values try: browser_type = get_config_value("browser", "type", "firefox") headless_mode = get_config_value("browser", "headless", "True") url_key = self.source_country_code + "-" + self.destination_country_code vfs_url = get_config_value("vfs-url", url_key) except KeyError as e: logging.error(f"Missing configuration value: {e}") return email_id = get_config_value("vfs-credential", "email") password = get_config_value("vfs-credential", "password") appointment_params = self.get_appointment_params(args) # Launch browser and perform actions with sync_playwright() as p: browser = getattr(p, browser_type).launch( headless=headless_mode in ("True", "true") ) page = browser.new_page() stealth_sync(page) page.goto(vfs_url) self.pre_login_steps(page) try: self.login(page, email_id, password) logging.info("Logged in successfully") except Exception: browser.close() raise LoginError( "\033[1;31mLogin failed. " + "Please verify your username and password by logging in to the browser and try again.\033[0m" ) logging.info(f"Checking appointments for {appointment_params}") appointment_found = False try: dates = self.check_for_appontment(page, appointment_params) if dates: # Log successful appointment finding logging.info( f"\033[1;32mFound appointments on: {', '.join(dates)} \033[0m" ) self.notify_appointment(appointment_params, dates) appointment_found = True else: # Log no appointments found logging.info( "\033[1;33mNo appointments found for the specified criteria.\033[0m" ) except Exception as e: logging.error(f"Appointment check failed: {e}") browser.close() return appointment_found def get_appointment_params(self, args: argparse.Namespace) -> Dict[str, str]: """ Collects appointment parameters from command-line arguments or user input. This method iterates through pre-defined `appointment_param_keys` (replace with relevant keys) and retrieves values either from provided arguments or prompts the user for input if values are missing. Args: args (argparse.Namespace): Namespace object containing parsed command-line arguments. Returns: Dict[str, str]: A dictionary containing appointment parameters. """ appointment_params = {} for key in self.appointment_param_keys: if ( getattr(args, "appointment_params") is not None and args.appointment_params[key] is not None ): appointment_params[key] = args.appointment_params[key] else: key_name = key.replace("_", " ") appointment_params[key] = input(f"Enter the {key_name}: ") return appointment_params def notify_appointment(self, appointment_params: Dict[str, str], dates: List[str]): """ Sends appointment dates notification to the user. This method is responsible for notifying the appointment dates to the user configured channels Args: dates (List[str]): A list of appointment dates. appointment_params (Dict[str, str]): A dictionary containing appointment search criteria. """ message = f"Found appointment(s) for {', '.join(appointment_params.values())} on {', '.join(dates)}" channels = get_config_value("notification", "channels") if len(channels) == 0: logging.warning( "No notification channels configured. Skipping notification." ) return for channel in channels.split(","): client = get_notification_client(channel) try: client.send_notification(message) except Exception: logging.error(f"Failed to send {channel} notification") @abstractmethod def login( self, page: playwright.sync_api.Page, email_id: str, password: str ) -> None: """ Performs login steps specific to the VFS website for the bot's country. This abstract method needs to be implemented by subclasses to handle country-specific login procedures (e.g., filling login form elements, handling CAPTCHAs). It should interact with the Playwright `page` object to achieve login functionality. Args: page (playwright.sync_api.Page): The Playwright page object used for browser interaction. email_id (str): The user's email address for VFS login. password (str): The user's password for VFS login. Raises: Exception: If login fails due to unexpected errors. """ raise NotImplementedError("Subclasses must implement login logic") @abstractmethod def pre_login_steps(self, page: playwright.sync_api.Page) -> None: """ Performs any pre-login steps required by the VFS website for the bot's country. This abstract method allows subclasses to implement country-specific actions that need to be done before login (e.g., cookie acceptance, language selection). It should interact with the Playwright `page` object to perform these actions. Args: page (playwright.sync_api.Page): The Playwright page object used for browser interaction. """ @abstractmethod def check_for_appontment( self, page: playwright.sync_api.Page, appointment_params: Dict[str, str] ) -> List[str]: """ Checks for appointments based on provided parameters on the VFS website. This abstract method needs to be implemented by subclasses to interact with the VFS website and search for appointments based on the given `appointment_params` dictionary. It should use the Playwright `page` object to navigate the website and extract appointment dates. Args: page (playwright.sync_api.Page): The Playwright page object used for browser interaction. appointment_params (Dict[str, str]): A dictionary containing appointment search criteria. Returns: List[str]: A list of available appointment dates (empty list if none found). """ raise NotImplementedError( "Subclasses must implement appointment checking logic" ) ================================================ FILE: vfs_appointment_bot/vfs_bot/vfs_bot_de.py ================================================ import logging from typing import Dict, List, Optional from playwright.sync_api import Page from vfs_appointment_bot.utils.date_utils import extract_date_from_string from vfs_appointment_bot.vfs_bot.vfs_bot import VfsBot class VfsBotDe(VfsBot): """Concrete implementation of VfsBot for Germany (DE). This class inherits from the base `VfsBot` class and implements country-specific logic for interacting with the VFS website for Germany. It overrides the following methods to handle German website specifics: - `login`: Fills the login form elements with email and password. - `pre_login_steps`: Rejects all cookie policies if presented. - `check_for_appontment`: Performs appointment search based on provided parameters and extracts available dates from the website. """ def __init__(self, source_country_code: str): """ Initializes a VfsBotDe instance for Germany. This constructor sets the source country code and the destination country code "de"(Germany). It also defines appointment parameter keys specific to the destination country's VFS website. Args: source_country_code (str): The country code where you're applying from. """ super().__init__() self.source_country_code = source_country_code self.destination_country_code = "DE" self.appointment_param_keys = [ "visa_center", "visa_category", "visa_sub_category", ] def login(self, page: Page, email_id: str, password: str) -> None: """ Performs login steps specific to the German VFS website. This method fills the email and password input fields on the login form and clicks the "Sign In" button. It raises an exception if the login fails (e.g., if the "Start New Booking" button is not found after login). Args: page (playwright.sync_api.Page): The Playwright page object used for browser interaction. email_id (str): The user's email address for VFS login. password (str): The user's password for VFS login. Raises: Exception: If login fails due to unexpected errors or missing "Start New Booking" button. """ email_input = page.locator("#mat-input-0") password_input = page.locator("#mat-input-1") email_input.fill(email_id) password_input.fill(password) page.get_by_role("button", name="Sign In").click() page.wait_for_selector("role=button >> text=Start New Booking") def pre_login_steps(self, page: Page) -> None: """ Performs pre-login steps specific to the German VFS website. This method checks for a "Reject All" button for cookie policies and clicks it if found. Args: page (playwright.sync_api.Page): The Playwright page object used for browser interaction. """ policies_reject_button = page.get_by_role("button", name="Reject All") if policies_reject_button is not None: policies_reject_button.click() logging.debug("Rejected all cookie policies") def check_for_appontment( self, page: Page, appointment_params: Dict[str, str] ) -> Optional[List[str]]: """ Checks for appointments on the German VFS website based on provided parameters. This method clicks the "Start New Booking" button, selects the specified visa center, category, and subcategory based on the `appointment_params` dictionary. It then extracts the available appointment dates from the website and returns them as a list. If no appointments are found, it returns None. Args: page (playwright.sync_api.Page): The Playwright page object used for browser interaction. appointment_params (Dict[str, str]): A dictionary containing appointment search criteria. Returns: Optional[List[str]]: A list of available appointment dates (empty list if none found), including a timestamp of the check, or None if no appointments found. """ page.get_by_role("button", name="Start New Booking").click() # Select Visa Centre visa_centre_dropdown = page.wait_for_selector("mat-form-field") visa_centre_dropdown.click() visa_centre_dropdown_option = page.wait_for_selector( f'mat-option:has-text("{appointment_params.get("visa_center")}")' ) visa_centre_dropdown_option.click() # Select Visa Category visa_category_dropdown = page.query_selector_all("mat-form-field")[1] visa_category_dropdown.click() visa_category_dropdown_option = page.wait_for_selector( f'mat-option:has-text("{appointment_params.get("visa_category")}")' ) visa_category_dropdown_option.click() # Select Subcategory visa_subcategory_dropdown = page.query_selector_all("mat-form-field")[2] visa_subcategory_dropdown.click() visa_subcategory_dropdown_option = page.wait_for_selector( f'mat-option:has-text("{appointment_params.get("visa_sub_category")}")' ) visa_subcategory_dropdown_option.click() try: page.wait_for_selector("div.alert") appointment_date_elements = page.query_selector_all("div.alert") appointment_dates = [] for appointment_date_element in appointment_date_elements: appointment_date_text = appointment_date_element.text_content() appointment_date = extract_date_from_string(appointment_date_text) if appointment_date is not None and len(appointment_date) > 0: appointment_dates.append(appointment_date) return appointment_dates except Exception: return None return None ================================================ FILE: vfs_appointment_bot/vfs_bot/vfs_bot_factory.py ================================================ from vfs_appointment_bot.vfs_bot.vfs_bot import VfsBot class UnsupportedCountryError(Exception): """Raised when an unsupported country code is provided.""" def get_vfs_bot(source_country_code: str, destination_country_code: str) -> VfsBot: """Retrieves the appropriate VfsBot class for a given country. This function searches for a matching subclass of `VfsBot` based on the provided destination country code (ISO 3166-1 alpha-2). If no matching class is found, an `UnsupportedCountryError` exception is raised. Args: source_country_code (str): The ISO 3166-1 alpha-2 country code where you're applying from. destination_country_code (str): The ISO 3166-1 alpha-2 country code where the appointment is needed. Returns: VfsBot: An instance of the `VfsBot` subclass specific to the provided country. Raises: UnsupportedCountryError: If the provided country is not supported. """ country_lower = destination_country_code if country_lower == "DE": from .vfs_bot_de import VfsBotDe return VfsBotDe(source_country_code) elif country_lower == "IT": from .vfs_bot_it import VfsBotIt return VfsBotIt(source_country_code) else: raise UnsupportedCountryError( f"Country {destination_country_code} is not supported" ) ================================================ FILE: vfs_appointment_bot/vfs_bot/vfs_bot_it.py ================================================ import logging from typing import Dict, List, Optional from playwright.sync_api import Page from vfs_appointment_bot.utils.date_utils import extract_date_from_string from vfs_appointment_bot.vfs_bot.vfs_bot import VfsBot class VfsBotIt(VfsBot): """Concrete implementation of VfsBot for Italy (IT). This class inherits from the base `VfsBot` class and implements country-specific logic for interacting with the VFS website for Italy. It overrides the following methods to handle Italy website specifics: - `login`: Fills the login form elements with email and password. - `pre_login_steps`: Rejects all cookie policies if presented. - `check_for_appontment`: Performs appointment search based on provided parameters and extracts available dates from the website. """ def __init__(self, source_country_code: str): """ Initializes a VfsBotIt instance for Italy. This constructor sets the source country code and the destination country code "it"(Italy). It also defines appointment parameter keys specific to the destination country's VFS website. Args: source_country_code (str): The country code where you're applying from. """ super().__init__() self.source_country_code = source_country_code self.destination_country_code = "IT" self.appointment_param_keys = [ "visa_center", "visa_category", "visa_sub_category", ] if self.source_country_code == "MA": self.appointment_param_keys.append("payment_mode") def login(self, page: Page, email_id: str, password: str) -> None: """ Performs login steps specific to the Italy VFS website. This method fills the email and password input fields on the login form and clicks the "Sign In" button. It raises an exception if the login fails (e.g., if the "Start New Booking" button is not found after login). Args: page (playwright.sync_api.Page): The Playwright page object used for browser interaction. email_id (str): The user's email address for VFS login. password (str): The user's password for VFS login. Raises: Exception: If login fails due to unexpected errors or missing "Start New Booking" button. """ email_input = page.locator("#mat-input-0") password_input = page.locator("#mat-input-1") email_input.fill(email_id) password_input.fill(password) page.get_by_role("button", name="Sign In").click() page.wait_for_selector("role=button >> text=Start New Booking") def pre_login_steps(self, page: Page) -> None: """ Performs pre-login steps specific to the Italy VFS website. This method checks for a "Reject All" button for cookie policies and clicks it if found. Args: page (playwright.sync_api.Page): The Playwright page object used for browser interaction. """ policies_reject_button = page.get_by_role("button", name="Reject All") if policies_reject_button is not None: policies_reject_button.click() logging.debug("Rejected all cookie policies") def check_for_appontment( self, page: Page, appointment_params: Dict[str, str] ) -> Optional[List[str]]: """ Checks for appointments on the Italy VFS website based on provided parameters. This method clicks the "Start New Booking" button, selects the specified visa center, category, and subcategory based on the `appointment_params` dictionary. It then extracts the available appointment dates from the website and returns them as a list. If no appointments are found, it returns None. Args: page (playwright.sync_api.Page): The Playwright page object used for browser interaction. appointment_params (Dict[str, str]): A dictionary containing appointment search criteria. Returns: Optional[List[str]]: A list of available appointment dates (empty list if none found), including a timestamp of the check, or None if no appointments found. """ page.get_by_role("button", name="Start New Booking").click() # Select Visa Centre visa_centre_dropdown = page.wait_for_selector("mat-form-field") visa_centre_dropdown.click() visa_centre_dropdown_option = page.wait_for_selector( f'mat-option:has-text("{appointment_params.get("visa_center")}")' ) visa_centre_dropdown_option.click() # Select Visa Category visa_category_dropdown = page.query_selector_all("mat-form-field")[1] visa_category_dropdown.click() visa_category_dropdown_option = page.wait_for_selector( f'mat-option:has-text("{appointment_params.get("visa_category")}")' ) visa_category_dropdown_option.click() # Select Subcategory visa_subcategory_dropdown = page.query_selector_all("mat-form-field")[2] visa_subcategory_dropdown.click() visa_subcategory_dropdown_option = page.wait_for_selector( f'mat-option:has-text("{appointment_params.get("visa_sub_category")}")' ) visa_subcategory_dropdown_option.click() if self.source_country_code == "MA": # Select Payment Mode payment_mode_dropdown = page.query_selector_all("mat-form-field")[3] payment_mode_dropdown.click() payment_mode_dropdown_option = page.wait_for_selector( f'mat-option:has-text("{appointment_params.get("payment_mode")}")' ) payment_mode_dropdown_option.click() try: page.wait_for_selector("div.alert") appointment_date_elements = page.query_selector_all("div.alert") appointment_dates = [] for appointment_date_element in appointment_date_elements: appointment_date_text = appointment_date_element.text_content() appointment_date = extract_date_from_string(appointment_date_text) if appointment_date is not None and len(appointment_date) > 0: appointment_dates.append(appointment_date) return appointment_dates except Exception: return None return None