Repository: erayerdin/tgcli (language: python)

Issue:
Completed Migration
With this PR, migration to Rust is minimally completed and the `development` branch will contain Rust as of now.
Add Verbosity
Add an argument for verbosity, such like `-vvv`.


Discussion / hints:

Fix patch:
diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index 15fdf3d..0000000
--- a/.coveragerc
+++ /dev/null
@@ -1,8 +0,0 @@
-[run]
-omit =
-    tgcli/cli/*
-    tests/*
-
-[report]
-exclude_lines =
-    pragma: no cover
diff --git a/.flake8 b/.flake8
deleted file mode 100644
index e04d66b..0000000
--- a/.flake8
+++ /dev/null
@@ -1,3 +0,0 @@
-[flake8]
-max-line-length = 160
-ignore = E231
diff --git a/.github/workflows/build.linux.yml b/.github/workflows/build.linux.yml
new file mode 100644
index 0000000..967472b
--- /dev/null
+++ b/.github/workflows/build.linux.yml
@@ -0,0 +1,46 @@
+name: build_linux
+
+on: [push, pull_request]
+
+jobs:
+  Test:
+    strategy:
+      matrix:
+        os: [ubuntu-latest]
+        rust: [stable, beta]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - uses: actions/checkout@v1
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: ${{ matrix.rust }}
+          override: true
+      - uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cargo/registry
+            ~/.cargo/git
+            target
+          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+      - uses: actions-rs/cargo@v1
+        env:
+          TELEGRAM_BOT_TOKEN: ${{ secrets.SECRET_TELEGRAM_BOT_TOKEN }}
+          TELEGRAM_RECEIVER: ${{ secrets.SECRET_TELEGRAM_RECEIVER }}
+          TELEGRAM_RECEIVER_ABSENT: ${{ secrets.SECRET_TELEGRAM_RECEIVER_ABSENT }}
+        with:
+          command: build
+          args: --release
+      - name: 'Upload Build'
+        uses: actions/upload-artifact@v2
+        with:
+          name: tgcli_ubuntu_x64
+          path: target/release/tgcli
+          retention-days: 1
+      - name: Release
+        uses: softprops/action-gh-release@v1
+        if: startsWith(github.ref, 'refs/tags/')
+        with:
+          files: target/release/tgcli
diff --git a/.github/workflows/build.macos.yml b/.github/workflows/build.macos.yml
new file mode 100644
index 0000000..b00794f
--- /dev/null
+++ b/.github/workflows/build.macos.yml
@@ -0,0 +1,41 @@
+name: build_macos
+
+on: [push, pull_request]
+
+jobs:
+  Test:
+    strategy:
+      matrix:
+        os: [macos-latest]
+        rust: [stable]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - uses: actions/checkout@v1
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: ${{ matrix.rust }}
+          override: true
+      - uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cargo/registry
+            ~/.cargo/git
+            target
+          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+      - uses: actions-rs/cargo@v1
+        env:
+          TELEGRAM_BOT_TOKEN: ${{ secrets.SECRET_TELEGRAM_BOT_TOKEN }}
+          TELEGRAM_RECEIVER: ${{ secrets.SECRET_TELEGRAM_RECEIVER }}
+          TELEGRAM_RECEIVER_ABSENT: ${{ secrets.SECRET_TELEGRAM_RECEIVER_ABSENT }}
+        with:
+          command: build
+          args: --release
+      - name: 'Upload Build'
+        uses: actions/upload-artifact@v2
+        with:
+          name: tgcli_macos_x64
+          path: target/release/tgcli
+          retention-days: 1
diff --git a/.github/workflows/build.windows.yml b/.github/workflows/build.windows.yml
new file mode 100644
index 0000000..5b07ace
--- /dev/null
+++ b/.github/workflows/build.windows.yml
@@ -0,0 +1,46 @@
+name: build_windows
+
+on: [push, pull_request]
+
+jobs:
+  Test:
+    strategy:
+      matrix:
+        os: [windows-latest]
+        rust: [stable]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - uses: actions/checkout@v1
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: ${{ matrix.rust }}
+          override: true
+      - uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cargo/registry
+            ~/.cargo/git
+            target
+          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+      - uses: actions-rs/cargo@v1
+        env:
+          TELEGRAM_BOT_TOKEN: ${{ secrets.SECRET_TELEGRAM_BOT_TOKEN }}
+          TELEGRAM_RECEIVER: ${{ secrets.SECRET_TELEGRAM_RECEIVER }}
+          TELEGRAM_RECEIVER_ABSENT: ${{ secrets.SECRET_TELEGRAM_RECEIVER_ABSENT }}
+        with:
+          command: build
+          args: --release
+      - name: 'Upload Build'
+        uses: actions/upload-artifact@v2
+        with:
+          name: tgcli_windows_x64
+          path: target/release/tgcli.exe
+          retention-days: 1
+      - name: Release
+        uses: softprops/action-gh-release@v1
+        if: startsWith(github.ref, 'refs/tags/')
+        with:
+          files: target/release/tgcli.exe
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
new file mode 100644
index 0000000..d64908f
--- /dev/null
+++ b/.github/workflows/coverage.yml
@@ -0,0 +1,39 @@
+name: coverage
+
+on: [push, pull_request]
+
+jobs:
+  Test:
+    strategy:
+      matrix:
+        os: [ubuntu-latest]
+        rust: [stable]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - uses: actions/checkout@v1
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: ${{ matrix.rust }}
+          override: true
+      - uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cargo/registry
+            ~/.cargo/git
+            target
+          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+      - name: Generate Coverage
+        env:
+          TELEGRAM_BOT_TOKEN: ${{ secrets.SECRET_TELEGRAM_BOT_TOKEN }}
+          TELEGRAM_RECEIVER: ${{ secrets.SECRET_TELEGRAM_RECEIVER }}
+          TELEGRAM_RECEIVER_ABSENT: ${{ secrets.SECRET_TELEGRAM_RECEIVER_ABSENT }}
+        run: |
+          cargo install cargo-tarpaulin
+          cargo tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml
+      - name: Upload Coverage
+        uses: codecov/codecov-action@v1
+        with:
+          fail_ci_if_error: true
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
new file mode 100644
index 0000000..4c33d65
--- /dev/null
+++ b/.github/workflows/format.yml
@@ -0,0 +1,35 @@
+name: format
+
+on: [push, pull_request]
+
+jobs:
+  Test:
+    strategy:
+      matrix:
+        os: [ubuntu-latest]
+        rust: [stable]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - uses: actions/checkout@v1
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: ${{ matrix.rust }}
+          override: true
+      - uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cargo/registry
+            ~/.cargo/git
+            target
+          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+      - uses: actions-rs/cargo@v1
+        env:
+          TELEGRAM_BOT_TOKEN: ${{ secrets.SECRET_TELEGRAM_BOT_TOKEN }}
+          TELEGRAM_RECEIVER: ${{ secrets.SECRET_TELEGRAM_RECEIVER }}
+          TELEGRAM_RECEIVER_ABSENT: ${{ secrets.SECRET_TELEGRAM_RECEIVER_ABSENT }}
+        with:
+          command: fmt
+          args: -- --check
diff --git a/.gitignore b/.gitignore
index 3770899..e80cfc7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,245 +1,4 @@
-
-# Created by https://www.gitignore.io/api/VisualStudioCode,python,pycharm+all,git,virtualenv
-# Edit at https://www.gitignore.io/?templates=VisualStudioCode,python,pycharm+all,git,virtualenv
-
-### Git ###
-# Created by git for backups. To disable backups in Git:
-# $ git config --global mergetool.keepBackup false
-*.orig
-
-# Created by git when using merge tools for conflicts
-*.BACKUP.*
-*.BASE.*
-*.LOCAL.*
-*.REMOTE.*
-*_BACKUP_*.txt
-*_BASE_*.txt
-*_LOCAL_*.txt
-*_REMOTE_*.txt
-
-### PyCharm+all ###
-# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
-# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
-
-# User-specific stuff
-.idea/**/workspace.xml
-.idea/**/tasks.xml
-.idea/**/usage.statistics.xml
-.idea/**/dictionaries
-.idea/**/shelf
-
-# Generated files
-.idea/**/contentModel.xml
-
-# Sensitive or high-churn files
-.idea/**/dataSources/
-.idea/**/dataSources.ids
-.idea/**/dataSources.local.xml
-.idea/**/sqlDataSources.xml
-.idea/**/dynamic.xml
-.idea/**/uiDesigner.xml
-.idea/**/dbnavigator.xml
-
-# Gradle
-.idea/**/gradle.xml
-.idea/**/libraries
-
-# Gradle and Maven with auto-import
-# When using Gradle or Maven with auto-import, you should exclude module files,
-# since they will be recreated, and may cause churn.  Uncomment if using
-# auto-import.
-# .idea/modules.xml
-# .idea/*.iml
-# .idea/modules
-
-# CMake
-cmake-build-*/
-
-# Mongo Explorer plugin
-.idea/**/mongoSettings.xml
-
-# File-based project format
-*.iws
-
-# IntelliJ
-out/
-
-# mpeltonen/sbt-idea plugin
-.idea_modules/
-
-# JIRA plugin
-atlassian-ide-plugin.xml
-
-# Cursive Clojure plugin
-.idea/replstate.xml
-
-# Crashlytics plugin (for Android Studio and IntelliJ)
-com_crashlytics_export_strings.xml
-crashlytics.properties
-crashlytics-build.properties
-fabric.properties
-
-# Editor-based Rest Client
-.idea/httpRequests
-
-# Android studio 3.1+ serialized cache file
-.idea/caches/build_file_checksums.ser
-
-### PyCharm+all Patch ###
-# Ignores the whole .idea folder and all .iml files
-# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
-
-.idea/
-
-# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
-
-*.iml
-modules.xml
-.idea/misc.xml
-*.ipr
-
-### Python ###
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-#  Usually these files are written by a python script from a template
-#  before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-.hypothesis/
-.pytest_cache/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-.python-version
-
-# celery beat schedule file
-celerybeat-schedule
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
+/target
 .env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-### Python Patch ###
-.venv/
-
-### VirtualEnv ###
-# Virtualenv
-# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
-[Bb]in
-[Ii]nclude
-[Ll]ib
-[Ll]ib64
-[Ll]ocal
-[Ss]cripts
-pyvenv.cfg
-pip-selfcheck.json
-
-### VisualStudioCode ###
-.vscode/*
-!.vscode/settings.json
-!.vscode/tasks.json
-!.vscode/launch.json
-!.vscode/extensions.json
-
-### VisualStudioCode Patch ###
-# Ignore all local history of files
-.history
-
-# End of https://www.gitignore.io/api/VisualStudioCode,python,pycharm+all,git,virtualenv
-.atomignore
+*.snap
+!.vscode
\ No newline at end of file
diff --git a/.isort.cfg b/.isort.cfg
deleted file mode 100644
index 98a8e50..0000000
--- a/.isort.cfg
+++ /dev/null
@@ -1,7 +0,0 @@
-[settings]
-known_third_party = click,colorful,pytest,requests,requests_mock,setuptools,yaspin
-multi_line_output=3
-include_trailing_comma=True
-force_grid_wrap=0
-use_parentheses=True
-line_length=88
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
deleted file mode 100644
index e816a49..0000000
--- a/.pre-commit-config.yaml
+++ /dev/null
@@ -1,25 +0,0 @@
-repos:
-  - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v2.0.0
-    hooks:
-      - id: trailing-whitespace
-      - id: end-of-file-fixer
-      - id: check-yaml
-      - id: check-added-large-files
-
-  - repo: https://github.com/python/black
-    rev: master
-    hooks:
-      - id: black
-  - repo: https://github.com/asottile/seed-isort-config
-    rev: master
-    hooks:
-      - id: seed-isort-config
-  - repo: https://github.com/pre-commit/mirrors-isort
-    rev: master
-    hooks:
-      - id: isort
-  - repo: https://gitlab.com/pycqa/flake8
-    rev: '3.7.9'
-    hooks:
-      -   id: flake8
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..0e0e3d0
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,20 @@
+# .readthedocs.yaml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Set the version of Python and other tools you might need
+build:
+  os: ubuntu-20.04
+  tools:
+    python: "3.9"
+
+mkdocs:
+  configuration: mkdocs.yml
+
+# Optionally declare the Python requirements required to build your docs
+python:
+   install:
+   - requirements: docs/requirements.txt
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index eabf221..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-language: python
-cache: pip
-dist: xenial
-
-jobs:
-  include:
-  - stage: Style Checking
-    python: '3.7'
-    install:
-    - pip install -r requirements.txt
-    # ref: https://stackoverflow.com/a/54053100/2926992
-    - cat dev.requirements.txt | sed -e '/^\s*#.*$/d' -e '/^\s*$/d' | xargs -n 1 pip install || true
-    script:
-    - black setup.py tests/ tgcli/
-    - flake8 setup.py tests/ tgcli/
-    - isort -c
-  - stage: Testing
-    python: '3.5'
-    install:
-    - pip install -r requirements.txt
-    - cat dev.requirements.txt | sed -e '/^\s*#.*$/d' -e '/^\s*$/d' | xargs -n 1 pip install || true
-    script:
-    - pytest
-  - stage: Testing
-    python: '3.6'
-    install:
-    - pip install -r requirements.txt
-    - cat dev.requirements.txt | sed -e '/^\s*#.*$/d' -e '/^\s*$/d' | xargs -n 1 pip install || true
-    script:
-    - pytest
-  - stage: Testing
-    python: '3.7'
-    install:
-    - pip install -r requirements.txt
-    - cat dev.requirements.txt | sed -e '/^\s*#.*$/d' -e '/^\s*$/d' | xargs -n 1 pip install || true
-    script:
-    - pytest
-  - stage: Covering / Deploy
-    python: '3.7'
-    install:
-    - pip install -r requirements.txt
-    - cat dev.requirements.txt | sed -e '/^\s*#.*$/d' -e '/^\s*$/d' | xargs -n 1 pip install || true
-    script:
-    - pytest --cov-config .coveragerc --cov tgcli tests/
-    - coveralls
-    deploy:
-      provider: pypi
-      user: erayerdin
-      password:
-        secure: "hzjEH6i5bmlxncQ97Df9ebnhIhLIZFPWNZpIUYrmte0vqLNUWNEKA9sF+Eb9C8bveMWbH8M10mj5tm4Oc/RUnEnOwTbjnrJxVgeoZZPT4RMMgTXcwc6WHlHCxEHheHcnozKUa6TkAdZtgmkKDHmqRvDyp9xhkkRTmTKyzHJYYiOIFa/p4TqGmtlH6Bj7m3YFwhPDbi8nPY4OVJ/wYdeywDFYMxxNQ3ugzM3xBDTWtn1u/9goeQRYKkNLc2jDztM9ZoFA+bKdFrBN0M38k1nmweJuVkvFk3+tAqN5+oRMYN9bE+AIAYWStOFJ9pfcAq/HOvZdWz9NGtX7fSm1l5R7H6x5XjevCB5aG9wkGokwoN5gpjfas6ndjaLBF2wkG+1wQ0ph2oaqswSLzD6CDs4XdNYwa5CvFnjLAGsNCx6J6riKDHhLjW5mca22reEe6ThNduxbP+Pz4WHcxXcTjoPvA2FGls69zrTmDyX1qXyQXGb6WoqijXio6QWU7fHnlSWAp/DrXW24tRhaySSdX0DwVkpn6hNFek0SGBjM+6dL34f5o0vQMwWpV9n3LnYXMEDiAGZc6P1uChTwl7d//wBSa+ZJzmUgyWPUigSz9Yb4lady+Idvxie+q21u9SLetvm/ktuhxDwdQn3RhcITAJ9LYFEm2neANapBg5vER2KNJjw="
-      on:
-        tags: true
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..4151e6d
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,14 @@
+{
+	// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
+	// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
+	// List of extensions which should be recommended for users of this workspace.
+	"recommendations": [
+		"rust-lang.rust-analyzer",
+		"Gruntfuggly.todo-tree",
+		"PeterSchmalfeldt.explorer-exclude",
+		"RedVanWorkshop.explorer-exclude-vscode-extension",
+		"jrobsonchase.rust-extension-pack",
+	],
+	// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
+	"unwantedRecommendations": []
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index ea70222..83d415d 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,20 +1,14 @@
 {
-    "python.linting.enabled": true,
-    "python.linting.pycodestyleEnabled": false,
-    "python.linting.pylintEnabled": false,
     "files.exclude": {
         "**/.git": true,
         "**/.svn": true,
         "**/.hg": true,
         "**/CVS": true,
         "**/.DS_Store": true,
-        "**/__pycache__": true,
-        ".venv": true,
-        ".vscode": true,
-        "*.egg-info": true,
-        ".pytest_cache": true
+        "**/Thumbs.db": true,
+        "**/*.lock": true,
+        "target": true
     },
     "explorerExclude.backup": null,
-    "python.pythonPath": ".venv/bin/python",
-    "python.linting.flake8Enabled": true
-}
+    "licenser.license": "MPLv2"
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..a8e0058
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,10 @@
+{
+    "version": "2.0.0",
+    "tasks": [
+        {
+            "label": "Server Docs",
+            "type": "shell",
+            "command": "mkdocs serve"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 729d505..a311163 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,146 +1,300 @@
 # Changelog
+
 All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
-and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+and this project adheres to
+[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [v0.5.1-alpha.1] - 2023-01-21
+
+### Changed
+
+ - Update `clap` to `3.2.23`.
+
+## [v0.5.0-alpha.9] - 2022-05-06
+
+### Added
+
+ - `bot send --protect-content` argument
+
+## [v0.5.0-alpha.8] - 2022-03-03
+
+### Changed
+
+- Exit codes have changed.
+- Changed `--verbose` argument from global to local.
+- Changed `bot --token` argument from global to local.
+- Changed `bot send --receiver` argument from global to local.
+- Changed `bot send --format` argument from global to local.
+- Changed `bot send --silent` argument from global to local.
+- The license from [Apache-2.0](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)) to [MPL-2.0](https://tldrlegal.com/license/mozilla-public-license-2.0-(mpl-2)).
+
+### Removed
+
+- `bot send video --width` argument
+- `bot send video --height` argument
+
+## [v0.5.0-alpha.7] - 2021-12-17
+
+### Changed
+
+- Migrated entire project to async.
+- Fixed exiting from SendOperation implementations.
+- Migrated to Rust 2021 edition.
+- Better error handling to clean up
+- Reverted to synchronous paradigm.
+
+## [v0.5.0-alpha.6]
+
+### Added
+
+- `-v` argument for verbosity
+
+### Changed
+
+- Non-verbose output is colorized now.
+
+## [v0.5.0-alpha.5] - 2021-11-24
+
+### Added
+
+- `send --silent` global argument helps you to supress notification sound on the
+  target device.
+
+## [v0.5.0-alpha.4] - 2021-11-23
+
+### Added
+
+- `bot send poll` has been implemented.
+- `bot send location` has been implemented.
+- `bot send video` has been implemented.
+- `bot send audio` has been implemented.
+
+### Changed
+
+- `bot send poll` now takes `question` as positional argument.
+- `bot send poll` now requires at least two `-o`/`--option` argument or it is
+  invalid.
+- `question` argument on `bot send poll` can only take 1-300 characters.
+- `-o`/`--option` on `bot send poll` can only take 1-100 characters.
+
+### Removed
+
+- `-q`/`--question` argument from `bot send poll`.
+- `-w`/`--width` argument from `bot send video`.
+- `-h`/`--height` argument from `bot send video`.
+
+## [v0.5.0-alpha.3] - 2021-11-19
+
+### Added
+
+- `bot send photo` has been implemented.
+
+## [v0.5.0-alpha.2] - 2021-11-19
+
+### Added
+
+- `bot send document` has been implemented.
+
+## [v0.5.0-alpha.1] - 2021-11-18
+
+### Added
+
+- `bot send message` has been reimplemented.
+- The same subcommands and arguments from the previous versions are inherited,
+  but they throw "not implemented yet".
+
+### Changed
+
+- _Totally migrated to Rust_ with a base foundation that might be subject to be
+  shaped further.
+
+## [v0.4.0pre4] - 2020-05-04
+
+### Added
+
+- Added `-m|--multiple` option to `bot send poll`.
+- Added `--anonymous/--no-anonymous` option to `bot send poll`.
+- Added `--until` option to `bot send poll`.
+
+## [v0.4.0pre3] - 2020-04-17
 
-## [0.4.0pre3] - 2020-04-17
 ### Changed
- - `--horizontal/-h` and `--vertical/-v` flags for `bot send video` has been
- changed to `--width/-w` ad `--height/-h`.
- - Default values for `--width/-w` and `--height/-h` flags on `bot send video`
- has been set to `1920` and `1080`.
- - Various bug fixes.
+
+- `--horizontal/-h` and `--vertical/-v` arguments for `bot send video` has been
+  changed to `--width/-w` ad `--height/-h`.
+- Default values for `--width/-w` and `--height/-h` arguments on
+  `bot send video` has been set to `1920` and `1080`.
+- Various bug fixes.
 
 ## [v0.3.1] - 2019-09-22
+
 ### Changed
- - The description of `--horizontal` and `--vertical` options on `bot send video`
- subcommand has been updated to imply that these values are going to be used for
- determining the aspect ratio of the video thumbnail.
- - Default values of `--horizontal` and `--vertical` options on `bot send video`
- has been set to `1`.
- - Only migrated all subcommands to their packages and modules.
- No significant change is present on the software.
+
+- The description of `--horizontal` and `--vertical` options on `bot send video`
+  subcommand has been updated to imply that these values are going to be used
+  for determining the aspect ratio of the video thumbnail.
+- Default values of `--horizontal` and `--vertical` options on `bot send video`
+  has been set to `1`.
+- Only migrated all subcommands to their packages and modules. No significant
+  change is present on the software.
 
 ## [v0.4.0pre2] - 2019-09-19
+
 ### Changed
- - The description of `--horizontal` and `--vertical` options on `bot send video`
- subcommand has been updated to imply that these values are going to be used for
- determining the aspect ratio of the video thumbnail.
- - Default values of `--horizontal` and `--vertical` options on `bot send video`
- has been set to `1`.
+
+- The description of `--horizontal` and `--vertical` options on `bot send video`
+  subcommand has been updated to imply that these values are going to be used
+  for determining the aspect ratio of the video thumbnail.
+- Default values of `--horizontal` and `--vertical` options on `bot send video`
+  has been set to `1`.
 
 ## [v0.4.0pre1] - 2019-09-18
+
 ### Changed
- - Only migrated all subcommands to their packages and modules.
- No significant change is present on the software.
+
+- Only migrated all subcommands to their packages and modules. No significant
+  change is present on the software.
 
 ## [v0.3.0] - 2019-04-28
+
 ### Added
- - `bot send photo` subcommand
- - `bot send video` subcommand
- - `bot send audio` subcommand
+
+- `bot send photo` subcommand
+- `bot send video` subcommand
+- `bot send audio` subcommand
 
 ### Changed
- - `bot send file` subcommand to `bot send document`
- - `bot send document` only sends *any* type of data
- - Moved `--format` option from all subcommands to `send`
+
+- `bot send file` subcommand to `bot send document`
+- `bot send document` only sends _any_ type of data
+- Moved `--format` option from all subcommands to `send`
 
 ### Removed
- - `bot send file` subcommand
- - `--format` option on `message`
- - `--format` option on `poll`
- - `--format` option on `location`
- - `--format` option on `document`
- - `--format` option on `photo`
- - `--format` option on `video`
- - `--format` option on `audio`
+
+- `bot send file` subcommand
+- `--format` option on `message`
+- `--format` option on `poll`
+- `--format` option on `location`
+- `--format` option on `document`
+- `--format` option on `photo`
+- `--format` option on `video`
+- `--format` option on `audio`
 
 ## [v0.2.1] - 2019-04-20
+
 ### Added
- - `location` to `send` in cli
+
+- `location` to `send` in cli
 
 ### Changed
- - Updated documentation for `location`
+
+- Updated documentation for `location`
 
 ## [v0.2.0] - 2019-04-19
+
 ### Added
- - `poll` section to *Bot* documentation
+
+- `poll` section to _Bot_ documentation
 
 ### Changed
- - `send` is now a group type of command which has `message`, `file` and `poll`
- subcommands.
- - Moved file sending functionality from `send` to `send file`
- - Moved message sending functionality from `send` to `send message`
- - `send` now only takes receiver chat id, other subcommands take related
- options and parameters
- - Updated *Bot* section of documentation
+
+- `send` is now a group type of command which has `message`, `file` and `poll`
+  subcommands.
+- Moved file sending functionality from `send` to `send file`
+- Moved message sending functionality from `send` to `send message`
+- `send` now only takes receiver chat id, other subcommands take related options
+  and parameters
+- Updated _Bot_ section of documentation
 
 ## [v0.1.2] - 2019-04-19
+
 ### Added
- - Downloads counter to README
+
+- Downloads counter to README
 
 ### Changed
- - Changes status from beta to stable
+
+- Changes status from beta to stable
 
 ### Removed
- - Status badge from README
+
+- Status badge from README
 
 ## [v0.1.1] - 2019-04-13
+
 ### Changed
- - Changed documentation theme to material.
- - Fixed issues on documentation.
+
+- Changed documentation theme to material.
+- Fixed issues on documentation.
 
 ## [v0.1.0b5] - 2019-03-14
+
 ### Changed
- - Download URL on `setup.py` now redirects to corresponding version `tar.gz`
- archive.
+
+- Download URL on `setup.py` now redirects to corresponding version `tar.gz`
+  archive.
 
 ## [v0.1.0b4] - 2019-01-22
 
 ### Added
- - Added `--as` flag to define the file type you sent.
+
+- Added `--as` argument to define the file type you sent.
 
 ## [v0.1.0b3] - 2019-01-22
 
 ### Changed
- - Moved `--secure/--no-secure` flag to root of app.
- - Moved `--token` flag to `bot` subcommand.
+
+- Moved `--secure/--no-secure` argument to root of app.
+- Moved `--token` argument to `bot` subcommand.
 
 ## [v0.1.0b2] - 2019-01-20
 
 ### Added
- - Added `--secure/--no-secure` flag in order to verify Telegram API HTTPS certificate.
+
+- Added `--secure/--no-secure` argument in order to verify Telegram API HTTPS
+  certificate.
 
 ### Changed
- - Fixed [issue](https://github.com/erayerdin/tgcli/issues/4) regarding OpenSSL issues on OSX.
+
+- Fixed [issue](https://github.com/erayerdin/tgcli/issues/4) regarding OpenSSL
+  issues on OSX.
 
 ## [v0.1.0b1] - 2019-01-19
 
 ### Changed
- - Fixed a possible minor bug about possible inconsistent response from Telegram BOT API.
+
+- Fixed a possible minor bug about possible inconsistent response from Telegram
+  BOT API.
 
 ## [v0.1.0a4] - 2019-01-18
 
 ### Changed
- - Updated `MANIFEST.in` for `tests_require` in `setup.py`.
+
+- Updated `MANIFEST.in` for `tests_require` in `setup.py`.
 
 ## [v0.1.0a3] - 2019-01-18
 
 ### Added
- - Added documentation.
- - Now `bot send` command accepts `-f/--file`, which sends file.
+
+- Added documentation.
+- Now `bot send` command accepts `-f/--file`, which sends file.
 
 ### Changed
- - `-f` flag in `bot send` now does not stand for `--format`, it stands for `--file`.
+
+- `-f` argument in `bot send` now does not stand for `--format`, it stands for
+  `--file`.
 
 ## [0.1.0a2] - 2019-01-15
 
 ### Added
- - Added `MANIFEST.in` to setup.
+
+- Added `MANIFEST.in` to setup.
 
 ## [0.1.0a1] - 2019-01-14
 
 ### Added
- - Added `bot` subcommand to main
- - Added `send` subcommand to `bot` command
+
+- Added `bot` subcommand to main
+- Added `send` subcommand to `bot` command
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..c3a267e
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1332 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "assert_cmd"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e996dc7940838b7ef1096b882e29ec30a3149a3a443cdc8dba19ed382eca1fe2"
+dependencies = [
+ "bstr",
+ "doc-comment",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bstr"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
+dependencies = [
+ "lazy_static",
+ "memchr",
+ "regex-automata",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
+
+[[package]]
+name = "bytes"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+
+[[package]]
+name = "cc"
+version = "1.0.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "3.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_derive",
+ "clap_lex",
+ "indexmap",
+ "once_cell",
+ "strsim",
+ "termcolor",
+ "textwrap",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "colored"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59"
+dependencies = [
+ "atty",
+ "lazy_static",
+ "winapi",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
+name = "doc-comment"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+
+[[package]]
+name = "dotenv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
+
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "fern"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9a4820f0ccc8a7afd67c39a0f1a0f4b07ca1725164271a64939d7aeb9af065"
+dependencies = [
+ "colored",
+ "log",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
+
+[[package]]
+name = "futures-task"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
+
+[[package]]
+name = "futures-timer"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
+
+[[package]]
+name = "futures-util"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "http"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
+
+[[package]]
+name = "httpdate"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
+
+[[package]]
+name = "hyper"
+version = "0.14.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
+
+[[package]]
+name = "itertools"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "js-sys"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+
+[[package]]
+name = "log"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "mio"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
+dependencies = [
+ "libc",
+ "log",
+ "miow",
+ "ntapi",
+ "winapi",
+]
+
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
+
+[[package]]
+name = "openssl"
+version = "0.10.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "os_str_bytes"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
+
+[[package]]
+name = "predicates"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c6ce811d0b2e103743eec01db1c50612221f173084ce2f7941053e94b6bb474"
+dependencies = [
+ "difflib",
+ "itertools",
+ "predicates-core",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "338c7be2905b732ae3984a2f40032b5e94fd8f52505b186c7d4d68d193445df7"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66d2927ca2f685faf0fc620ac4834690d29e7abb153add10f5812eef20b5e280"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "lazy_static",
+ "log",
+ "mime",
+ "mime_guess",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-native-tls",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
+name = "rstest"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b07f2d176c472198ec1e6551dc7da28f1c089652f66a7b722676c2238ebc0edf"
+dependencies = [
+ "futures",
+ "futures-timer",
+ "rstest_macros",
+ "rustc_version",
+]
+
+[[package]]
+name = "rstest_macros"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7229b505ae0706e64f37ffc54a9c163e11022a6636d58fe1f3f52018257ff9f7"
+dependencies = [
+ "cfg-if",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn",
+ "unicode-ident",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "schannel"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+dependencies = [
+ "lazy_static",
+ "winapi",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012"
+
+[[package]]
+name = "serde"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
+
+[[package]]
+name = "socket2"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "rand",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "termtree"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13a4ec180a2de59b57434704ccfad967f789b12737738798fa08798cd5824c16"
+
+[[package]]
+name = "textwrap"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
+
+[[package]]
+name = "tgcli"
+version = "0.5.1-alpha.1"
+dependencies = [
+ "assert_cmd",
+ "clap",
+ "dotenv",
+ "fern",
+ "log",
+ "mime",
+ "mime_guess",
+ "reqwest",
+ "rstest",
+ "serde",
+ "serde_json",
+ "url",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "pin-project-lite",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
+
+[[package]]
+name = "tracing"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
+dependencies = [
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
+name = "unicase"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
+
+[[package]]
+name = "web-sys"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "winreg"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
+dependencies = [
+ "winapi",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..a2f2639
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+name = "tgcli"
+version = "0.5.1-alpha.1"
+authors = ["Eray Erdin <eraygezer.94@gmail.com>"]
+edition = "2021"
+description = "A Telegram Terminal Application"
+license = "MPL-2.0"
+
+[dependencies]
+log = "0.4.14"
+mime = "0.3.16"
+mime_guess = "2.0.3"
+url = "2.2.2"
+serde_json = "1.0.71"
+
+[dependencies.clap]
+version = "3.2.23"
+features = ["derive", "env"]
+
+[dependencies.fern]
+version = "0.6.0"
+features = ["colored"]
+
+[dependencies.reqwest]
+version = "0.11.6"
+features = ["multipart", "json", "blocking"]
+
+[dependencies.serde]
+version = "1.0.130"
+features = ["derive"]
+
+[dev-dependencies]
+assert_cmd = "2.0.2"
+dotenv = "0.15.0"
+rstest = "0.16.0"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f4bbcd2
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in 
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
deleted file mode 100644
index f4db2b4..0000000
--- a/LICENSE.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-Copyright 2020 Eray Erdin
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 0f9ae12..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,3 +0,0 @@
-include README.md
-include requirements.txt
-include dev.requirements.txt
diff --git a/README.md b/README.md
index b5f5956..2e38d9f 100644
--- a/README.md
+++ b/README.md
@@ -1,61 +1,98 @@
 # tgcli
 
-[![PyPI - Version][badge_version]][pypi_url]
-[![PyPI - Downloads Last Month][badge_downloads]][pypi_url]
-[![PyPI - Python Versions][badge_pyversions]][pypi_url]
-[![PyPI - License][badge_license]][pypi_url]
+![Latest Stable Release][stable_version_badge]
+![Latest Release][latest_version_badge]
+![Github Release Download Count][github_download_counter_badge]
+![License][license_badge]
 [![Docs Status][badge_docs]][docs_url]
-[![Code Style][badge_style]](https://github.com/ambv/black)
 
-![](resources/recording.gif)
+`tgcli` is a terminal app to send messages with bots on Telegram.
 
-`tgcli` is a Python cli app for Telegram.
-
-[pypi_url]: https://pypi.org/project/tgcli/
 [docs_url]: https://tgcli.readthedocs.io
 
-[badge_version]: https://img.shields.io/pypi/v/tgcli.svg?style=flat-square&logo=python&logoColor=white
-[badge_downloads]: https://img.shields.io/pypi/dm/tgcli.svg?style=flat-square&logo=python&logoColor=white
-[badge_pyversions]: https://img.shields.io/pypi/pyversions/tgcli.svg?style=flat-square&logo=python&logoColor=white
-[badge_license]: https://img.shields.io/pypi/l/tgcli.svg?style=flat-square
+[stable_version_badge]: https://img.shields.io/github/v/release/erayerdin/tgcli?label=stable&style=flat-square
+[latest_version_badge]: https://img.shields.io/github/v/release/erayerdin/tgcli?include_prereleases&label=latest&style=flat-square
+[github_download_counter_badge]: https://img.shields.io/github/downloads/erayerdin/tgcli/total?logo=github&style=flat-square
+[license_badge]: https://img.shields.io/badge/license-MPL--2.0-lightgrey?style=flat-square
 [badge_docs]: https://img.shields.io/readthedocs/tgcli?style=flat-square
-[badge_style]: https://img.shields.io/badge/style-black-000000.svg?style=flat-square
 
 
-|              | Build | Coverage |
-|--------------|-------|----------|
-| **Master**   | [![Master Build][badge_build_master]][build_url] | [![Master Coverage][badge_coverage_master]][coverage_url] |
-| **Development** | [![Development Build][badge_build_development]][build_url] | [![Development Coverage][badge_coverage_development]][coverage_url] |
+|         | Master                                                                                                                                  | Development                                                                                                                                                 | Format                         | Coverage                           |
+| ------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ---------------------------------- |
+| Linux   | ![Linux Build Status on Master][linux_build_status_master_badge] ![Linux Test Status on Master][linux_test_status_master_badge]         | ![Linux Build Status on Development][linux_build_status_development_badge] ![Linux Test Status on Development][linux_test_status_development_badge]         | ![Format Status][format_badge] | ![Coverage Report][coverage_badge] |
+| MacOS   | ![MacOS Build Status on Master][macos_build_status_master_badge] ![MacOS Test Status on Master][macos_test_status_master_badge]         | ![MacOS Build Status on Development][macos_build_status_development_badge] ![MacOS Test Status on Development][macos_test_status_development_badge]         |                                |                                    |
+| Windows | ![Windows Build Status on Master][windows_build_status_master_badge] ![Windows Test Status on Master][windows_test_status_master_badge] | ![Windows Build Status on Development][windows_build_status_development_badge] ![Windows Test Status on Development][windows_test_status_development_badge] |                                |                                    |
+
+The details of the build status can be seen [here][build_url]. The details of the coverage report can be seen [here][coverage_url].
 
-[build_url]: https://travis-ci.com/erayerdin/tgcli
+[build_url]: https://github.com/erayerdin/tgcli/actions
 [coverage_url]: https://coveralls.io/github/erayerdin/tgcli
 
-[badge_build_master]: https://img.shields.io/travis/com/erayerdin/tgcli/master.svg?style=flat-square&logo=travis&logoColor=white
-[badge_build_development]: https://img.shields.io/travis/com/erayerdin/tgcli/development.svg?style=flat-square&logo=travis&logoColor=white
-[badge_coverage_master]: https://img.shields.io/coveralls/github/erayerdin/tgcli/master.svg?logo=star&logoColor=white&style=flat-square
-[badge_coverage_development]: https://img.shields.io/coveralls/github/erayerdin/tgcli/development.svg?logo=star&logoColor=white&style=flat-square
+<!-- Linux Badges -->
+
+[linux_build_status_master_badge]: https://img.shields.io/github/actions/workflow/status/erayerdin/tgcli/build.linux.yml?branch=master&style=flat-square
+[linux_build_status_development_badge]: https://img.shields.io/github/actions/workflow/status/erayerdin/tgcli/build.linux.yml?branch=development&style=flat-square
+
+[linux_test_status_master_badge]: https://img.shields.io/github/actions/workflow/status/erayerdin/tgcli/test.linux.yml?branch=master&style=flat-square
+[linux_test_status_development_badge]: https://img.shields.io/github/actions/workflow/status/erayerdin/tgcli/test.linux.yml?branch=development&style=flat-square
+
+<!-- MacOS Badges -->
+
+[macos_build_status_master_badge]: https://img.shields.io/github/actions/workflow/status/erayerdin/tgcli/build.macos.yml?branch=master&style=flat-square
+[macos_build_status_development_badge]: https://img.shields.io/github/actions/workflow/status/erayerdin/tgcli/build.macos.yml?branch=development&style=flat-square
+
+[macos_test_status_master_badge]: https://img.shields.io/github/actions/workflow/status/erayerdin/tgcli/test.macos.yml?branch=master&style=flat-square
+[macos_test_status_development_badge]: https://img.shields.io/github/actions/workflow/status/erayerdin/tgcli/test.macos.yml?branch=development&style=flat-square
+
+<!-- Windows Badges -->
+
+[windows_build_status_master_badge]: https://img.shields.io/github/actions/workflow/status/erayerdin/tgcli/build.windows.yml?branch=master&style=flat-square
+[windows_build_status_development_badge]: https://img.shields.io/github/actions/workflow/status/erayerdin/tgcli/build.windows.yml?branch=development&style=flat-square
+
+[windows_test_status_master_badge]: https://img.shields.io/github/actions/workflow/status/erayerdin/tgcli/test.windows.yml?branch=master&style=flat-square
+[windows_test_status_development_badge]: https://img.shields.io/github/actions/workflow/status/erayerdin/tgcli/test.windows.yml?branch=development&style=flat-square
+
+<!-- Miscellaneous Badges -->
+
+[format_badge]: https://img.shields.io/github/actions/workflow/status/erayerdin/tgcli/format.yml?branch=development&style=flat-square
+[coverage_badge]: https://img.shields.io/coveralls/github/erayerdin/tgcli/master?logo=linux&logoColor=white&style=flat-square
 
 ## Installing
 
-### pip (No auto-update)
+The project is in active development and is in alpha phase. Before installing tgcli, please consider these:
 
-Install via `pip`:
+ - There might be breaking changes on commands and behavior.
+ - There might be unexpected and unwanted behavior.
 
-```bash
-pip install tgcli
+### Snap Store
+
+The project is available on [Snap Store](https://snapcraft.io/tgcli). However, please consider that the project is alpha phase, installing it is a bit different:
+
+```
+sudo snap install tgcli --edge --devmode
+```
+
+`--edge` flag is used due to it is still in alpha phase and `--devmode` is used because `tgcli` requires networking in the confined environment.
+
+Also, consider that the apps with on the edge channel on Snap Store is not updated automatically. So, if you want to get a new release, you need to uninstall first then reinstall it again.
+
+```
+sudo snap remove tgcli
+sudo snap install tgcli --edge --devmode
 ```
-### tgcli (Auto-update, Arch-based)
 
-Also available as a package in [AUR](https://aur.archlinux.org/packages/tgcli/).
+
+### Install with [Guix](https://guix.gnu.org)
+
+tgcli is [available](https://guix.gnu.org/en/packages/tgcli-0.3.1/) in official channel of Guix.
 
 ```bash
-# assuming you use yay
-yay -S tgcli
+guix install tgcli
 ```
 
 ## Rationale
 
-[A similar project](https://github.com/vysheng/tg), built on Python, was created by [@vysheng](https://github.com/vysheng), but it has not been updated since 2016 and considered abandoned. This tool *is not a fork* of the mentioned project, it is built from ground up.
+[A similar project](https://github.com/vysheng/tg), built on Python, was created by [@vysheng](https://github.com/vysheng), but it has not been updated since 2016 and considered abandoned. This tool *is not a fork* of the mentioned project, it is built from ground up **with Rust**.
 
 ## Example
 
@@ -65,9 +102,7 @@ For now, the use case is pretty simple. To send a message:
 tgcli bot --token "BotToken" send --receiver "UserID" message "Your message"
 ```
 
-You don't need to expose your token as flag. If you set
-`TELEGRAM_BOT_TOKEN` environment variable, you do not need to set
-`--token` flag. Just set it before using `tgcli`:
+You don't need to expose your token as argument. If you set `TELEGRAM_BOT_TOKEN` environment variable, you do not need to set `--token` argument. Just set it before using `tgcli`:
 
 ```bash
 export TELEGRAM_BOT_TOKEN="BotToken"
@@ -79,15 +114,8 @@ You can get more information by doing:
 tgcli bot send --help
 ```
 
-Also, this repository uses notification from a private bot, you can see the
-example Travis configuration [here](.travis.yml). Private `TELEGRAM_BOT_TOKEN`
-and `TELEGRAM_RECEIVER` environment variables were set.
-
-This application serves a really small purpose for now. It might face
-breaking changes in the future.
+This application serves a really small purpose for now. It might face breaking changes in the future.
 
 ## Documentation
 
-Documentation has an intensive amount of  information about how to
-use `tgcli`. Refer to the
-[documentation][docs_url].
+Documentation has an intensive amount of  information about how to use `tgcli`. Refer to the [documentation][docs_url].
diff --git a/dev.requirements.txt b/dev.requirements.txt
deleted file mode 100644
index 3ec0a59..0000000
--- a/dev.requirements.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-pytest==5.4.1
-requests-mock==1.5.2
-mkdocs==1.0.4
-mkdocs-material==4.1.1
-pytest-click==0.3
-pytest-cov==2.8.1
-coveralls==2.0.0
-black==19.10b0
-flake8==3.7.9
-isort==4.3.21
-pre-commit==2.2.0
diff --git a/docs/bot.md b/docs/bot.md
index d6e9b66..530f625 100644
--- a/docs/bot.md
+++ b/docs/bot.md
@@ -2,235 +2,235 @@
 
 ## Introduction
 
-Bots automate messaging in Telegram. They immitate an account / real person.
-However, their username always finishes with `bot` suffix. In `tgcli`, bot
-actions are executed under `tgcli bot` command. To get offline help, use:
+Bots automate messaging in Telegram. They immitate an account / real person. However, their username always finishes with `bot` suffix. In `tgcli`, bot actions can be invoked under `tgcli bot` subcommand. To get offline help, use:
 
-    tgcli bot --help
+```bash
+tgcli bot --help
+```
 
-`bot` subcommand also has options that you might be interested.
+`bot` subcommand also has arguments that you might be interested.
 
-Short Flag | Full Flag | Required/Optional | Description
---- | --- | --- | ---
--t | --token | Required[^1] | Token of bot.
+| Short Flag | Full Flag | Required/Optional | Description   |
+| ---------- | --------- | ----------------- | ------------- |
+| -t         | --token   | Required[^1]      | Token of bot. |
 
 [^1]: It is not required if you have `TELEGRAM_BOT_TOKEN` environment variable
       set in your current shell session.
 
 !!! tip
-    You can also pass `TELEGRAM_BOT_TOKEN` environment variable to current
-    session of your terminal in order to protect your token from being exposed
-    regularly.
+    You can also set `TELEGRAM_BOT_TOKEN` environment variable to current session of your terminal in order to protect your token from being exposed regularly.
 
-        tgcli bot -t "YourBotToken" send -r "somebody" "message"
+        tgcli bot send message "hello" -t "YourBotToken" --receiver "somebody"
         # or better
         export TELEGRAM_BOT_TOKEN="YourBotToken"
-        tgcli bot send -r "somebody" "message"
+        tgcli bot send message "hello" --receiver "somebody"
+        # or even better
+        TELEGRAM_BOT_TOKEN="YourBotToken" tgcli bot send message "hello" --receiver "somebody"
 
 ## send
 
-`send` is a subcommand of `bot` which, hence the name, operate sending
+`send` is a subcommand of `bot` which, hence the name, invoke sending
 operations. To get help:
 
     tgcli bot send --help
 
-`send` has only one option:
+`send` has the arguments below:
 
-Short Flag | Full Flag | Required/Optional | Description
---- | --- | --- | ---
--r | --receiver | Required | The receiver's ID, an integer.
- | --format | Optional | The format of message. Choices are `markdown` and `html`. Default is `markdown`.
+| Short Flag | Full Flag         | Required/Optional | Description                                                                                               |
+|------------|-------------------|-------------------|-----------------------------------------------------------------------------------------------------------|
+| -r         | --receiver        | Required          | The receiver's ID, an integer.                                                                            |
+|            | --format          | Optional          | The format of message. Choices are `markdown` and `html`. Default is `markdown`.[^markdown_format_choice] |
+|            | --silent          | Optional          | The message will not play notification sound on target device if present.                                 |
+|            | --protect-content | Optional          | Do not let user to forward or save the message.                                                           |
 
-After you define the receiver's ID, then you can use any subcommand of `send`.
-To give an example:
+After you find out the receiver's ID, then you can use any subcommand of `send`. To give an example:
 
-    tgcli bot send -r $RECEIVER_ID message "Hello, world!"
+```bash
+# assuming receiver id is 1234
+tgcli bot send -r 1234 message "Hello, world!"
+# or --receiver
+```
 
-Assuming you have replaced `$RECEIVER_ID` with a user or group id, this is how
-you send a message.
+!!! tip
+    ID of a user  *is not* username or human-readable name. It is an unsigned 64-bit integer representing the account. To get your ID, send [@userinfobot](https://t.me/userinfobot) *any* message and it will provide you *your own* user id.
 
-!!! note
-    You *have to* define `-r`/`--receiver`  in order to use any subcommand of
-    `send`, even if what you need is only `--help`.
+If you'd like to send a message without the notification sound playing on the target device, you can use `--silent` global argument to supress the sound.
 
-!!! tip
-    ID of a user  *is not* username or human-readable name. It is an integer
-    representing the account. To get your ID, send
-    [@userinfobot](https://t.me/userinfobot) *any* message.
+```bash
+tgcli bot send message "foo" -r 1234 --silent
+```
+
+!!! warning
+    `--silent` argument does not disable notification, it only supresses the notification sound. The user will still see the notification on device *unless the user willingly disabled the notifications from your bot*.
+
+If you'd like to send any kind of message but would not like any users to forward or save them, you can use `--protect-content` argument.
+
+```bash
+tgcli bot send message "foo" -r 1234 --protect-content
+```
+
+[^markdown_format_choice]: By default, [MarkdownV2](https://core.telegram.org/bots/api#markdownv2-style) style is used.
 
 ### message
 
-`message` is a subcommand of `send` command and is used to send *regular*
-messages. To get help:
+`message` is a subcommand of `send` command and is used to send *regular* messages. To get help:
 
-    tgcli bot send -r $RECEIVER_ID message --help
+```bash
+tgcli bot send message --help
+```
 
-`message` has the options and parameters below:
+`message` has the arguments below:
 
-Short Flag | Full Flag | Required/Optional | Description
---- | --- | --- | ---
- | message | Required | The message.
+| Short Flag | Full Flag | Required/Optional | Description                 |
+| ---------- | --------- | ----------------- | --------------------------- |
+| message    | Required  | Required          | The content of the message. |
 
 In order to send a message, do:
 
-    tgcli bot send -r $RECEIVER_ID message "foo"
+```bash
+tgcli bot send --receiver 1234 message "foo"
+```
 
 `--format` helps you define the format of your message. See the example:
 
-    tgcli bot send -r $RECEIVER_ID message "<b>bold</b>" --format html
+```bash
+tgcli bot send --receiver 1234 --format html message "<b>bold</b>"
+```
 
 !!! warning
-    Since Telegram also targets the mobile environment, it is safe to assume
-    that not all features and/or tags of markdown and/or HTML are supported.
-    Before using different features or tags, see
-    [this part of the official bot API documentation][telegram_bot_api_markdown]
-    for *Markdown* and
-    [this part of the official bot API documentation][telegram_bot_api_html] for
-    *HTML* in order to see the limitations.
-
-[telegram_bot_api_markdown]: https://core.telegram.org/bots/api#markdown-style
+    Since Telegram also targets the mobile environment, it is safe to assume that not all features and/or tags of markdown and/or HTML are supported. Before using different features or tags, see [this part of the official bot API documentation][telegram_bot_api_markdown] for *Markdown* and [this part of the official bot API documentation][telegram_bot_api_html] for *HTML* in order to review the limitations.
+
+[telegram_bot_api_markdown]: https://core.telegram.org/bots/api#markdownv2-style
 [telegram_bot_api_html]: https://core.telegram.org/bots/api#html-style
 
 ### document
 
-`document` is a subcommand of `send` and is used to send files through `tgcli`. To
-get help:
+`document` is a subcommand of `send` and is used to send files through `tgcli`. To get help:
 
-    tgcli bot send -r $RECEIVER_ID file --help
+```bash
+tgcli bot send document --help
+```
 
-!!! Warning
-    A file sent by `document` subcommand is *only
-    downloadable*, which means it will not have traits of several
-    media types in Telegram such as play button, full-screen view
-    or keyboard navigation etc.
+!!! Tip
+    A file sent by `document` subcommand is *only downloadable*, which means it will not have traits of several media types in Telegram such as play button, full-screen view or keyboard navigation etc.
 
-`document` has the options and parameters below:
+`document` owns the arguments below:
 
-Short Flag | Full Flag | Required/Optional | Description
---- | --- | --- | ---
--m | --message | Optional | The message.
- | --thumbnail | Optional | An image file to set thumbnail.
- | file | Required | Path to file.
+| Short Flag | Full Flag   | Required/Optional | Description                       |
+| ---------- | ----------- | ----------------- | --------------------------------- |
+|            | file        | Required          | Path to file.                     |
+| -m         | --message   | Optional          | The message[^doc_msg_char_limit]. |
+|            | --thumbnail | Optional          | An image file to set thumbnail.   |
 
 In order to send a file, do:
 
-    tgcli bot send -r $RECEIVER_ID file path/to/file
+```bash
+tgcli bot send --receiver 1234 document path/to/file
+```
+
+[^doc_msg_char_limit]: The current limit for messages on documents, videos, audios or photos is limited to at most 1024 characters by Telegram. You can see caption fields of all document-related endpoints, [such as this one](https://core.telegram.org/bots/api#sendphoto).
 
 #### File Storage Limits
 
-The file storage limit for `document` is 50 megabytes
-for other files
-[as stated in the documentation](https://core.telegram.org/bots/api#sending-files)
-Also, when you send a file, the message is limited to have up to 1024
-characters.
+The file storage limit for `document` is 50 megabytes for other files [as stated in the documentation](https://core.telegram.org/bots/api#sending-files).
+
+While we don't know how long the files are kept in the server, it is safe to assume that Telegram server will wipe files depending on:
 
-While we don't know how long the files are kept in the server, it is safe to
-assume that Telegram server will wipe files depending on:
- - when they are accessed,
- - if they are forwarded or saved and/or
- - when the last login of forwarders or savers was
- - et cetera
+ - when the last time they were accessed was,
+ - their forward/save count
+ - the last login time of forwarders or savers was etc.
 
-And it is even *safer to assume that bots' files will be higher priority in
-wiping operations*. That's why it is a good practice to forward the files sent
-by bots to *Saved Messages*, even better to backup them to a storage that you
-own if these files have higher importance to you.
+And it is even *safer to assume that bots' files will have higher priority in wiping operations*[^no_report_on_file_wiping]. That's why it is a good practice to forward the files sent by bots to *Saved Messages*, even better to backup them to a storage that you own if these files have higher importance to you.
+
+[^no_report_on_file_wiping]: One should keep in mind that there have been few reports of data loss complaint from Telegram's side to this day.
 
 ### photo
 
-`photo` is a subcommand of `send` and is used to send photos
-through `tgcli`. To get help:
+`photo` is a subcommand of `send` and is used to send photos through `tgcli`. To get help:
 
-    tgcli bot send -r $RECEIVER_ID photo --help
+```bash
+tgcli bot send photo --help
+```
 
 !!! info
-    A file sent by `photo` subcommand (i) can be viewed
-    **full-screen** *with one click/touch* and (ii) is
-    **can be navigated** *with arrow keys on the keyboard or
-    swiping*.
+    A file sent by `photo` subcommand has these traits:
+    
+    1. It can be viewed **full-screen** *with one click/touch*.
+    2. It can be **navigated** *with arrow keys on the keyboard or
+    swiping on the touch screen*.
 
-`photo` has the options and parameters below:
+`photo` subcommand owns these arguments:
 
-Short Flag | Full Flag | Required/Optional | Description
---- | --- | --- | ---
--m | --message | Optional | The message.
- | file | Required | Path to file.
+| Short Flag | Full Flag | Required/Optional | Description   |
+| ---------- | --------- | ----------------- | ------------- |
+|            | file      | Required          | Path to file. |
+| -m         | --message | Optional          | The message.  |
 
 The usage is similar to the usage of [document](bot.md#document).
 
 ### video
 
-`video` is a subcommand of `send` and is used to send videos
-through `tgcli`. To get help:
+`video` is a subcommand of `send` and is used to send videos through `tgcli`. To get help:
 
-    tgcli bot send -r $RECEIVER_ID video --help
+```bash
+tgcli bot send video --help
+```
 
 !!! info
-    A file sent by `video` subcommand can be viewed
-    **full-screen** *with a button*.
+    A file sent by `video` subcommand can be viewed **full-screen** *with a button*.
 
-`video` has the options and parameters below:
+`video` has the arguments below:
 
-Short Flag | Full Flag | Required/Optional | Description
---- | --- | --- | ---
--m | --message | Optional | The message.
--w | --width | Optional | The width of the video.
--v | --vertical | Optional | The height of the video.
- | file | Required | Path to file.
+| Short Flag | Full Flag | Required/Optional | Description   |
+| ---------- | --------- | ----------------- | ------------- |
+|            | file      | Required          | Path to file. |
+| -m         | --message | Optional          | The message.  |
 
 The usage is similar to the usage of [document](bot.md#document).
 
-!!! warning
-    Defining width and height with their related flags do not
-    actually change your video content. It only shapes the
-    video container with the aspect ratio of the width and
-    height in the application. For more information, see
-    [#27][issue_27] and [#40][issue_40].
-
-[issue_27]: https://github.com/erayerdin/tgcli/issues/27
-[issue_40]: https://github.com/erayerdin/tgcli/issues/40
-
 ### audio
 
-`audio` is a subcommand of `send` and is used to send audios
-through `tgcli`. To get help:
+`audio` is a subcommand of `send` and is used to send audios through `tgcli`. To get help:
 
-    tgcli bot send -r $RECEIVER_ID audio --help
+```bash
+tgcli bot send audio --help
+```
 
 !!! info
-    A file sent by `audio` subcommand **can be played** *with a
-    play button*.
+    A file sent by `audio` subcommand **can be played** *with a play button*.
 
-`audio` has the options and parameters below:
+`audio` owns the arguments below:
 
-Short Flag | Full Flag | Required/Optional | Description
---- | --- | --- | ---
--m | --message | Optional | The message.
- | --performer | Optional | The performer of audio.
- | --title | Optional | The title of audio.
- | file | Required | Path to file.
+| Short Flag | Full Flag   | Required/Optional | Description             |
+| ---------- | ----------- | ----------------- | ----------------------- |
+|            | file        | Required          | Path to file.           |
+| -m         | --message   | Optional          | The message.            |
+|            | --performer | Optional          | The performer of audio. |
+|            | --title     | Optional          | The title of audio.     |
 
 The usage is similar to the usage of [document](bot.md#document).
 
 ### poll
 
-`poll` is a subcommand of `send` and is used to publish polls. To get help:
+`poll` is a subcommand of `send` and is used to send polls. To get help:
 
-    tgcli bot send -r $RECEIVER_ID poll --help
+```bash
+tgcli bot send poll --help
+```
 
-`poll` has the options and parameters below:
+`poll` has these arguments:
 
-Short Flag | Full Flag | Required/Optional | Description
---- | --- | --- | ---
--o | --option | Required[^2] | A single option for poll. You can define multiple options.
- | question | Required | The question for poll.
-
-!!! warning
-    You cannot send polls to private chats but only to groups and channels.
+| Short Flag | Full Flag | Required/Optional | Description                                                |
+| ---------- | --------- | ----------------- | ---------------------------------------------------------- |
+|            | question  | Required          | The question for poll.                                     |
+| -o         | --option  | Required[^2]      | A single option for poll. You can define multiple options. |
 
-To publish a poll:
+To start a poll:
 
-    tgcli bot send -r $RECEIVER_ID poll "Am I a ghost?" -o "Yes" -o "No"
+```bash
+# a plain poll
+tgcli bot send --receiver 1234 poll "Am I a ghost?" -o "Yes" -o "No"
+```
 
 !!! note
     Also keep in mind the the order of `-o`/`--option` is preserved for polls.
@@ -239,14 +239,15 @@ To publish a poll:
 
 ### location
 
-`location` is a subcommand of `send` and is used to send a location. To get
-help:
+`location` is a subcommand of `send` and is used to send a location. To get help:
 
-    tgcli bot send -r $RECEIVER_ID location --help
+```bash
+tgcli bot send location --help
+```
 
-`location` has the options and parameters below:
+`location` owns the arguments below:
 
-Short Flag | Full Flag | Required/Optional | Description
---- | --- | --- | ---
--x | --latitude | Required | Latitude on the world map. A float.
--y | --longitude | Required | Longtitude on the world map. A float.
+| Short Flag | Full Flag   | Required/Optional | Description                           |
+| ---------- | ----------- | ----------------- | ------------------------------------- |
+| -x         | --latitude  | Required          | Latitude on the world map. A float.   |
+| -y         | --longitude | Required          | Longtitude on the world map. A float. |
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 14271ca..0d873c3 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -2,9 +2,17 @@
 
 ## Installation
 
-The easiest way to install `tgcli` is to use `pip`:
+`tgcli` is currently being migrated to Rust and is in its alpha stage. You can install it via Snap.
 
-    pip install tgcli
+[![Snap Store](https://raw.githubusercontent.com/snapcore/snap-store-badges/master/EN/%5BEN%5D-snap-store-white.svg)](https://snapcraft.io/tgcli)
+
+However, since it is in alpha stage, the installation differs from a normal Snap installation.
+
+```bash
+sudo snap install tgcli --edge --devmode
+```
+
+Also, again, due to being in alpha stage, it does not update automatically like a regular Snap package does. In order to get the latest version, you have to reinstall it as above.
 
 ## A Basic Usage
 
@@ -14,33 +22,30 @@ In this tutorial, our goal is to send ourselves a message using a bot.
 
 #### Creating A Bot
 
-In order to create a bot, you will need to contact
-[@BotFather](https://t.me/BotFather). Steps are:
+In order to create a bot, you will need to contact [@BotFather](https://t.me/BotFather). Steps are:
 
  - Send `/newbot` to `@BotFather`.
  - Give a human-readable name for your bot.
  - Give a computer-readable username for your bot.
 
-Then `@BotFather` will provide you a token for HTTP API. Note it down to
-somewhere.
+Then `@BotFather` will provide you a token for HTTP API. Note it down to somewhere.
 
-Now, search your bot with the username you've just set. Send the bot an initial
-message.
+Now, search your bot with the username you've just set. Send the bot an initial message.
 
 !!! Reason
     Bots cannot initiate conversation in Telegram. So you cannot send a message
-    to somebody if they didn't send you one.
+    to somebody if they didn't send you one. That's why we first need to send
+    a message to the bot we've just created.
 
 #### Getting User ID from Telegram
 
 We need to know our own user ID for bot to send us a message.
 
-!!! Info
+!!! Warning
     ID is *different from your human-readable name* or your username (the one
     that starts with (at) sign). It is an integer.
 
-In order to get it, you need to send [@userinfobot](https://t.me/userinfobot)
-any message and the bot will reply you with your information:
+In order to get it, you need to send [@userinfobot](https://t.me/userinfobot) any message and the bot will reply you with your information:
 
 ```
 @foo
@@ -53,26 +58,33 @@ Take note of your ID.
 
 ### Sending the Message
 
-Now, we have (i) token of our bot and (ii) our user ID. You are pretty much
-ready to go with these. Open up a terminal and...
+Now, we have (i) token of our bot and (ii) our user ID. You are pretty much ready to go with these. Open up a terminal and...
 
-```
+```bash
 tgcli bot --token "BotToken" send --receiver "1234567890" message "Message"
 # or better
 tgcli bot -t "BotToken" send -r "1234567890" message "Message"
 ```
 
-You can also set `TELEGRAM_BOT_TOKEN` environment variable to protect your
-bot token in the current terminal session. For more information about how to
-use bot, refer to [Bot](bot.md) section.
+You can also set `TELEGRAM_BOT_TOKEN` environment variable to protect your bot token in the current terminal session.
+
+```bash
+TELEGRAM_BOT_TOKEN="BotToken" tgcli bot send -r "1234567890" message "Message"
+```
+
+For more information about how to use bot, refer to [Bot](bot.md) section.
 
 ## Getting Offline Information
 
 You can use below to get usage information without referring to this
 documentation everytime:
 
-    tgcli --help
+```bash
+tgcli --help
+```
 
 Or you can use any subcommand's help, as an example of `bot` subcommand:
 
-    tgcli bot --help
+```bash
+tgcli bot --help
+```
diff --git a/docs/img/mbtc_custom_qr.png b/docs/img/mbtc_custom_qr.png
deleted file mode 100644
index 0d4f3a5..0000000
Binary files a/docs/img/mbtc_custom_qr.png and /dev/null differ
diff --git a/docs/img/mbtc_d1_qr.png b/docs/img/mbtc_d1_qr.png
deleted file mode 100644
index dc64739..0000000
Binary files a/docs/img/mbtc_d1_qr.png and /dev/null differ
diff --git a/docs/img/travisci-example-project-page.png b/docs/img/travisci-example-project-page.png
deleted file mode 100644
index 4521b5b..0000000
Binary files a/docs/img/travisci-example-project-page.png and /dev/null differ
diff --git a/docs/img/travisci-example-settings-page.png b/docs/img/travisci-example-settings-page.png
deleted file mode 100644
index 7ea48a3..0000000
Binary files a/docs/img/travisci-example-settings-page.png and /dev/null differ
diff --git a/docs/img/xmr_custom_qr.png b/docs/img/xmr_custom_qr.png
deleted file mode 100644
index 122f4f2..0000000
Binary files a/docs/img/xmr_custom_qr.png and /dev/null differ
diff --git a/docs/img/xmr_d1_qr.png b/docs/img/xmr_d1_qr.png
deleted file mode 100644
index b3496fe..0000000
Binary files a/docs/img/xmr_d1_qr.png and /dev/null differ
diff --git a/docs/index.md b/docs/index.md
index 191ef0d..f076d77 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -2,22 +2,24 @@
 
 ## Introduction
 
-[![PyPI](https://img.shields.io/pypi/pyversions/tgcli.svg?style=flat-square&logo=python&logoColor=white)][pypi_url]
-[![PyPI](https://img.shields.io/pypi/l/tgcli.svg?style=flat-square)][pypi_url]
-[![Telegram](https://img.shields.io/badge/telegram-%40erayerdin-%2332afed.svg?style=flat-square&logo=telegram&logoColor=white)](https://t.me/erayerdin)
-[![Code Style](https://img.shields.io/badge/style-black-000000.svg?style=flat-square)](https://github.com/ambv/black)
+![Latest Stable Release][stable_version_badge]
+![Latest Release][latest_version_badge]
+![Github Release Download Count][github_download_counter_badge]
+![License][license_badge]
 
-[pypi_url]: https://pypi.org/project/tgcli/
+[stable_version_badge]: https://img.shields.io/github/v/release/erayerdin/tgcli?label=stable&style=flat-square
+[latest_version_badge]: https://img.shields.io/github/v/release/erayerdin/tgcli?include_prereleases&label=latest&style=flat-square
+[github_download_counter_badge]: https://img.shields.io/github/downloads/erayerdin/tgcli/total?logo=github&style=flat-square
+[license_badge]: https://img.shields.io/badge/license-Apache--2.0-lightgrey?style=flat-square
 
-`tgcli` is a Telegram client that you can use on terminal. You can automate
-repetitive tasks on Telegram with `tgcli`.
+`tgcli` is a Telegram client that you can use on terminal. You can automate repetitive tasks such as sending a message, photo, video, audio, location or poll using `tgcli`.
 
 ## License
 
 `tgcli` is licensed under Apache License 2.0.
 
 ```
-Copyright 2019 Eray Erdin
+Copyright 2020 Eray Erdin
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -31,36 +33,3 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 ```
-
-## Donations
-
-`tgcli` is a free (as in beer and speech) software that I have 
-built in my leisure time and been maintaining it. If you like to
-use it, please consider a small donation.
-
-### How do I donate?
-
-I accept donations in:
-
- - MiliBitcoin (mBTC)
- - Monero (XMR)
-
-You can donate predefined (*almost* $1) or custom donation by 
-clicking or scanning below.
-
-| | Predefined (*Almost* $1) | How Much You Wish |
-|-|--------------------------|-------------------|
-| MiliBitcoin (mBTC) | [![mBTC predefined][mbtc_d1_qr]][mbtc_d1] | [![mBTC custom][mbtc_custom_qr]][mbtc_custom] |
-| Monero (XMR)       | [![XMR predefined][xmr_d1_qr]][xmr_d1] | [![XMR custom][xmr_custom_qr]][xmr_custom]
-
-[mbtc_d1]: bitcoin:bc1qpjqftgzvr2cstrn7lkfl7q84h0uq6k806cv9md?amount=0.00009732&message=For%20%22tgcli%22%20as%20my%20gratitude.&time=1568582579
-[mbtc_d1_qr]: img/mbtc_d1_qr.png
-
-[mbtc_custom]: bitcoin:bc1qcywffxwa0rxsszgm07cyvsksxm3jxqj8z80ezh?message=For%20%22tgcli%22%20as%20my%20gratitude.&time=1568746229
-[mbtc_custom_qr]: img/mbtc_custom_qr.png
-
-[xmr_d1]: monero://44Fs67hkoVxA9xrcLHTS4zfcYehBtzxo8LLcNWWaJ1HHT6SA6FN6aqai4QKfY7gU6TL65Pp46ov1aBy4E6jpV7ohRyfFbBr?tx_amount=0.015&tx_message=For%20%22tgcli%22%20as%20my%20gratitude.
-[xmr_d1_qr]: img/xmr_d1_qr.png
-
-[xmr_custom]: monero://44Fs67hkoVxA9xrcLHTS4zfcYehBtzxo8LLcNWWaJ1HHT6SA6FN6aqai4QKfY7gU6TL65Pp46ov1aBy4E6jpV7ohRyfFbBr?tx_message=For%20%22tgcli%22%20as%20my%20gratitude.
-[xmr_custom_qr]: img/xmr_custom_qr.png
\ No newline at end of file
diff --git a/docs/practical.md b/docs/practical.md
index f4bbb55..860b812 100644
--- a/docs/practical.md
+++ b/docs/practical.md
@@ -7,13 +7,11 @@ This page contains some examples about how to use `tgcli` practically.
 !!! warning
     This section is about Linux.
 
-`$HOME/.profile` script is run each time you log in as a particular user in a
-machine. This might be useful if you want to get a notification if someone logs
-in to your server.
+`$HOME/.profile` script is run each time you log in as a particular user in a machine. This might be useful if you want to get a notification if someone logs into your server.
 
 Add line below to `$HOME/.profile`:
 
-```sh
+```bash
 tgcli bot -t "BotToken" send -r "ReceiverID" message "🔑 Someone logged in as **$USER** on **$(date)**."
 ```
 
@@ -21,97 +19,3 @@ Now you will receive a message each time someone logs in to your server:
 
 !!! example
     🔑 Someone logged in as **foo** on **Fri Sep 20 00:21:46 +03 2019**.
-
-## Getting Notifications about TravisCI Builds
-
-You can receive notifications about your TravisCI builds with `tgcli`. Since it
-is plain bash and you can use environment variables, you can use
-[default environment variables][travisci_environment_variables]
-of TravisCI in order to get extensive information about your builds. That means
-you can give links to the related commit, branch, issue, pull request or the
-build itself on the message as well because you can format your message with
-either Markdown or HTML.
-
-[travisci_environment_variables]: https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
-
-### Preparations
-
-You need to prepare your environment in order to work with `tgcli`. The very
-first step is to set up environment variables.
-
-You can set up environment variables in your TravisCI builds and the good thing
-is that these variables can be easily *obscured*.
-
-!!! note
-    This is pretty useful to us because we probably would like to hide the token
-    of our bot and the ID number of the receiver.
-
-We need to set up two variables beforehand, one is *your bot's token* and *the
-user id of the receiver*. You can get your bot's token from [BotFather][bot_father]
-and the id of the receiver from [UserInfoBot][user_info_bot].
-
-We will set `TELEGRAM_BOT_TOKEN` with the token of our bot `TELEGRAM_RECEIVER`
-with the id of the receiver. In order to do that, go to "Settings" on your
-TravisCI project.
-
-![go to settings from project page](img/travisci-example-project-page.png)
-
-Then fill in the related fields on "Environment Variables" section.
-
-![fill in the environment variables section](img/travisci-example-settings-page.png)
-
-!!! note
-    Make sure "Display Value in Build Log" is turned off so that
-    `TELEGRAM_BOT_TOKEN` and `TELEGRAM_RECEIVER` are not visible in the
-    build log.
-
-Now you are ready to go.
-
-[bot_father]: https://t.me/BotFather
-[user_info_bot]: https://t.me/userinfobot
-
-### Getting A Grasp
-
-Let's start with a simple example. Your jobs are built upon stages and a simple
-stage with the usage of `tgcli` looks as below:
-
-```yaml
- - stage: Your Stage Name
-   install:
-    # your other installations
-    - pip install tgcli # or tgcli==X.Y.Z if you prefer a version
-   script:
-    - tgcli bot send -r $TELEGRAM_RECEIVER message "📏 Starting tests..."
-    # do your testing here
-   after_success:
-    - tgcli bot send -r $TELEGRAM_RECEIVER message "✅ Tests are successful."
-   after_failure:
-    - tgcli bot send -r $TELEGRAM_RECEIVER message "📦🔥 Tests have failed!"
-```
-
-!!! tip
-    You do not have to pass `TELEGRAM_BOT_TOKEN` variables explicitly since
-    it is already used by `tgcli`.
-
-### Customizing the Messages
-
-TravisCI has many useful [environment variables][travisci_environment_variables]
-built right in. You can use these environment variables to customize your
-messages.
-
-For instance, instead of we say plain "Starting tests..." when we start a job,
-we can give URL to the current job as below:
-
-```yaml
- - stage: Your Stage
- # other stuff
- script:
-  - tgcli bot send -r $TELEGRAM_RECEIVER message "Starting job [#$TRAVIS_JOB_NUMBER]($TRAVIS_JOB_WEB_URL) of $TRAVIS_BUILD_STAGE_NAME stage..."
- # other stuff
-```
-
-This will give a link to your current job with the number of job and the name of
-the stage.
-
-!!! example
-    Starting job [#207.4](https://travis-ci.com/erayerdin/tgcli/jobs/237169789) of Testing stage...
\ No newline at end of file
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..84b5a90
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,5 @@
+mkdocs
+mkdocs-material
+mike
+mkdocs-git-revision-date-localized-plugin
+pymdown-extensions
diff --git a/docs/root.md b/docs/root.md
index 18aadf0..8d551de 100644
--- a/docs/root.md
+++ b/docs/root.md
@@ -1,26 +1,78 @@
 # Root
 
-Root of `tgcli` application is itself. The root simply does nothing, which
-means you have to use one of the subcommands below for further functionality:
+Root of `tgcli` application is itself.
+
+| Short Flag | Full Flag | Required/Optional     | Description               |
+| ---------- | --------- | --------------------- | ------------------------- |
+| -v         | --verbose | Optional and Multiple | Sets the verbosity level. |
+
+The root simply does nothing, which means you have to use one of the subcommands below for further functionality:
 
  - [bot](bot.md)
 
-## Global Options
+## Verbosity
+
+You can set the verbosity level multiple times to get more outputs.
+
+```bash
+# 3 level verbosity
+tgcli bot send message "foo" -r 1234 -vvv
+```
+
+Each level increases the verbosity level a little bit. Below shows what tgcli shows on each level:
+
+| Verbosity Level | Log Levels                      | Colorized | Level Prefix | Location Prefix | Location Target |
+| --------------- | ------------------------------- | --------- | ------------ | --------------- | --------------- |
+| 0               | Info, Warn, Error               | ✅         | ❌            | ❌               | tgcli           |
+| 1               | Debug, Info, Error, Warn        | ❌         | ✅            | ❌               | tgcli           |
+| 2               | Debug, Info, Error, Warn        | ❌         | ✅            | ✅               | tgcli           |
+| 3               | Trace, Debug, Info, Error, Warn | ❌         | ✅            | ✅               | tgcli           |
+| 4               | Trace, Debug, Info, Error, Warn | ❌         | ✅            | ✅               | All             |
+
+### Verbosity Levels
+
+Verbosity level represents how many times `-v` argument is used. `-vv` means verbosity level is 2 while the absense means verbosity level is 0.
+
+### Log Levels
+
+Each level defines has different purposes.
+
+ - **Trace**: The values of some important variables.
+ - **Debug**: Low-level logs of operation e.g. converting types.
+ - **Info**: Logs targeting the end-user. This is the default in 0 verbosity level.
+ - **Warn**: Logs that were needed but did not cause the program the halt due to having a default or fallback value to progress.
+ - **Error**: Logs that caused the program to not be able to progress further and immediately halt.
+
+!!! warning
+    This is how levels are used by tgcli. The libraries it depends is not in our control and might use the levels for different purposes.
+
+### Colorization
+
+tgcli colorizes the entire line depending on the verbosity level.
+
+The default verbosity level, info, does not have a color value, resulting in the default color of the terminal. The other levels have predefined colors by tgcli.
+
+ - *Error* results in **bright red** color.
+ - *Warn* results in **yellow** color.
+ - *Debug* results in **blue** color.
+ - *Trace* results in **cyan** color.
+
+!!! warning
+    The colors are disabled if the verbosity level is set above 0.
+
+### Prefixes
+
+Prefixes are some metadata prepended to the logging output.
 
-Root contains some global options that you might be interested.
+```plain
+[PREFIX1][PREFIX2] message
+```
 
-Short Flag | Full Flag | Required/Optional | Description
---- | --- | --- | ---
- | --secure/--no-secure | Optional | Whether to verify HTTPS certificate or not. Default is `True` except OSX[^1].
+There are two kinds of prefixes in tgcli:
 
-[^1]: OSX might be bundled with an older version of OpenSSL, which
-      requires to point at certificate manually and causes `tgcli` to fail.
-      That's why the requests in OSX is `--no-secure` by default. Easiest way
-      to overcome this problem is to [update OpenSSL on your system](https://apple.stackexchange.com/a/126832). Then manually trigger `--secure` flag everytime.
+ - **Level Prefix**: It contains level name e.g. `[INFO]`.
+ - **Location Prefix**: It contains where the error originates from e.g. `[tgcli::operation]`.
 
-### Examples
+### Location
 
-    # note that this will not work
-    tgcli --no-secure whatever
-    # you need to set secure option after root for it to work properly
-    # this will make every request non-secure on its subcommand
+Not all logs originate from tgcli, some logs originate from other libraries such as reqwest, mio, want etc.
diff --git a/mkdocs.yml b/mkdocs.yml
index c149d48..be1c1f4 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -6,33 +6,50 @@ repo_url: https://github.com/erayerdin/tgcli
 
 theme:
   name: material
+  language: en
+  features:
+    - navigation.instant
+    - navigation.tracking
+    - navigation.sections
+    - navigation.top
+    - search
+    - search.highlight
+    - search.share
   palette:
     primary: cyan
     accent: cyan
 
 extra:
+  version:
+    provider: mike
   social:
-    - type: github
+    - icon: fontawesome/brands/github
       link: https://github.com/erayerdin
-    - type: twitter
+    - icon: fontawesome/brands/twitter
       link: https://twitter.com/_erayerdin
-    - type: linkedin
-      link: https://linkedin.com/in/erayerdin
-    - type: telegram
+    - icon: fontawesome/brands/telegram
       link: https://t.me/erayerdin
+    - icon: fontawesome/solid/globe
+      link: https://erayerdin.com
+      name: Personal Blog
   disqus: erayerdin
 
 markdown_extensions:
   - admonition
+  - pymdownx.details
+  - pymdownx.superfences
   - codehilite:
       guess_lang: false
   - toc:
       permalink: true
   - footnotes
 
+plugins:
+  - git-revision-date-localized
+
 nav:
     - Home: index.md
     - Getting Started: getting-started.md
     - Root: root.md
     - Bot: bot.md
-    - Practical Usage: practical.md
\ No newline at end of file
+    - Practical Usage: practical.md
diff --git a/nfpm.yaml b/nfpm.yaml
new file mode 100644
index 0000000..8a82c8b
--- /dev/null
+++ b/nfpm.yaml
@@ -0,0 +1,33 @@
+# nfpm example config file
+#
+# check https://nfpm.goreleaser.com/configuration for detailed usage
+#
+name: "tgcli"
+arch: "amd64"
+platform: "linux"
+version: "v0.5.0-alpha.6"
+section: "default"
+priority: "extra"
+provides:
+- tgcli
+depends:
+- libc6
+maintainer: "Eray Erdin <eraygezer.94@gmail.com>"
+description: |
+  A terminal application for Telegram.
+  It can send messages, photos, videos, audios, locations or polls with bots.
+vendor: "Eray Erdin"
+homepage: "http://github.com/erayerdin/tgcli"
+license: "Apache-2.0"
+contents:
+- src: ./target/release/tgcli
+  dst: /usr/local/bin/tgcli
+# overrides:
+#   rpm:
+#     scripts:
+#       preinstall: ./scripts/preinstall.sh
+#       postremove: ./scripts/postremove.sh
+#   deb:
+#     scripts:
+#       postinstall: ./scripts/postinstall.sh
+#       preremove: ./scripts/preremove.sh
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index d4b2bda..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-requests
-Click
-yaspin
-colorful
diff --git a/resources/coogger.jpg b/resources/coogger.jpg
deleted file mode 100644
index 3fd46f1..0000000
Binary files a/resources/coogger.jpg and /dev/null differ
diff --git a/resources/recording.gif b/resources/recording.gif
deleted file mode 100644
index 8f02ff0..0000000
Binary files a/resources/recording.gif and /dev/null differ
diff --git a/setup.py b/setup.py
deleted file mode 100644
index e5274f2..0000000
--- a/setup.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from setuptools import setup
-
-import tgcli
-
-with open("README.md", "r") as f:
-    README = f.read()
-
-with open("requirements.txt", "r") as f:
-    DEPS = f.readlines()
-
-with open("dev.requirements.txt", "r") as f:
-    TEST_DEPS = f.readlines()
-
-GITHUB_RELEASE_URL = "https://github.com/erayerdin/tgcli/archive/v{}.tar.gz"
-
-setup(
-    name="tgcli",
-    version=tgcli.__version__,
-    description="tgcli is a client tool for Telegram.",
-    long_description=README,
-    long_description_content_type="text/markdown",
-    url="https://github.com/erayerdin/tgcli",
-    download_url=GITHUB_RELEASE_URL.format(tgcli.__version__),
-    packages=("tgcli", "tgcli.request", "tgcli.cli", "tgcli.cli.bot"),
-    entry_points="""
-        [console_scripts]
-        tgcli=tgcli.cli:cli
-    """,
-    include_package_data=True,
-    keywords="telegram messaging communication terminal tool cli",
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Intended Audience :: Developers",
-        "Intended Audience :: System Administrators",
-        "License :: OSI Approved :: Apache Software License",
-        "Programming Language :: Python :: 3 :: Only",
-        "Programming Language :: Python :: 3.5",
-        "Programming Language :: Python :: 3.6",
-        "Programming Language :: Python :: 3.7",
-        "Topic :: Communications :: Chat",
-    ],
-    author=tgcli.__author__,
-    author_email="eraygezer.94@gmail.com",
-    license="Apache License 2.0",
-    tests_require=TEST_DEPS,
-    install_requires=DEPS,
-    zip_safe=False,
-)
diff --git a/snapcraft.yaml b/snapcraft.yaml
new file mode 100644
index 0000000..8519c6a
--- /dev/null
+++ b/snapcraft.yaml
@@ -0,0 +1,24 @@
+name: tgcli
+version: git
+summary: A Telegram Terminal Application
+description: |
+  A terminal application for Telegram. You can send
+  messages, documents, photos, videos, location or
+  polls with tgcli.
+
+base: core18
+confinement: devmode
+
+parts:
+  tgcli:
+    stage-packages:
+      - openssl
+      - pkg-config
+    build-packages:
+      - libssl-dev
+    plugin: rust
+    source: .
+
+apps:
+  tgcli:
+    command: bin/tgcli
\ No newline at end of file
diff --git a/src/cli/logging.rs b/src/cli/logging.rs
new file mode 100644
index 0000000..423da99
--- /dev/null
+++ b/src/cli/logging.rs
@@ -0,0 +1,116 @@
+use fern::colors::{Color, ColoredLevelConfig};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+pub(crate) fn set_logger(
+    // Defines verbosity level.
+    // 0 - Info, Warn, Error + Self Target
+    // 1 - Debug, Info, Warn, Error + Self Target + Level Labels
+    // 2 - Debug, Info, Warn, Error + Self Target + Level Labels + Location Labels
+    // 3 - Trace, Debug, Info, Warn, Error + Self Target + Level Labels + Location Labels
+    // 4 - Trace, Debug, Info, Warn, Error + All Targets + Level Labels + Location Labels
+    verbosity: u8,
+) -> Result<(), log::SetLoggerError> {
+    let colors = ColoredLevelConfig::new()
+        .error(Color::BrightRed)
+        .warn(Color::Yellow)
+        .debug(Color::Blue)
+        .trace(Color::Cyan);
+
+    fern::Dispatch::new()
+        .filter(move |metadata| {
+            if verbosity >= 4 {
+                true
+            } else {
+                metadata.target().starts_with("tgcli")
+            }
+        })
+        // stdout chain
+        .chain(
+            fern::Dispatch::new()
+                .filter(|metadata| {
+                    metadata.level() != log::LevelFilter::Error
+                        || metadata.level() != log::LevelFilter::Warn
+                })
+                .format(move |out, message, record| {
+                    // if cfg!(debug_assertions) {
+                    //     out.finish(format_args!(
+                    //         "[{}][{}] {}",
+                    //         record.level(),
+                    //         record.target(),
+                    //         message
+                    //     ))
+                    // } else {
+                    //     match record.level() {
+                    //         log::Level::Error => out.finish(format_args!("Error: {}", message)),
+                    //         log::Level::Warn => out.finish(format_args!("Warning: {}", message)),
+                    //         _ => out.finish(format_args!("{}", message)),
+                    //     }
+                    // }
+                    match verbosity {
+                        1 => out.finish(format_args!(
+                            "[{levelname}] {message}",
+                            levelname = record.level(),
+                            message = message,
+                        )),
+                        2..=u8::MAX => out.finish(format_args!(
+                            "[{levelname}][{targetname}] {message}",
+                            levelname = record.level(),
+                            targetname = record.target(),
+                            message = message,
+                        )),
+                        0 => out.finish(format_args!(
+                            "\x1B[{}m{}\x1B[0m",
+                            colors.get_color(&record.level()).to_fg_str(),
+                            message
+                        )),
+                    }
+                })
+                .level(
+                    // if cfg!(debug_assertions) {
+                    //     log::LevelFilter::Trace
+                    // } else {
+                    //     log::LevelFilter::Info
+                    // }
+                    match verbosity {
+                        0 => log::LevelFilter::Info,
+                        1 | 2 => log::LevelFilter::Debug,
+                        _ => log::LevelFilter::Trace,
+                    },
+                )
+                .chain(std::io::stdout()),
+        )
+        // stderr chain
+        .chain(
+            fern::Dispatch::new()
+                .filter(|metadata| {
+                    metadata.level() == log::LevelFilter::Error
+                        || metadata.level() == log::LevelFilter::Warn
+                })
+                .format(move |out, message, record| match verbosity {
+                    1 => out.finish(format_args!(
+                        "[{levelname}] {message}",
+                        levelname = record.level(),
+                        message = message,
+                    )),
+                    2..=u8::MAX => out.finish(format_args!(
+                        "[{levelname}][{targetname}] {message}",
+                        levelname = record.level(),
+                        targetname = record.target(),
+                        message = message,
+                    )),
+                    0 => out.finish(format_args!(
+                        "\x1B[{colorbyte}m{message}\x1B[0m",
+                        colorbyte = colors.get_color(&record.level()).to_fg_str(),
+                        message = message,
+                    )),
+                })
+                .level(log::LevelFilter::Warn)
+                .chain(std::io::stderr()),
+        )
+        .apply()
+}
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
new file mode 100644
index 0000000..23ea8b0
--- /dev/null
+++ b/src/cli/mod.rs
@@ -0,0 +1,255 @@
+use std::path;
+
+use crate::{
+    cli::{
+        logging::set_logger,
+        validators::{
+            audio_validator, caption_validator, file_presence_validator, image_validator,
+            poll_option_validator, poll_question_validator, video_validator,
+        },
+    },
+    operations::{
+        bot::{
+            send::{
+                audio::{AudioParams, SendAudioOperation},
+                document::{DocumentParams, SendDocumentOperation},
+                location::{LocationParams, SendLocationOperation},
+                message::{MessageParams, SendMessageOperation},
+                photo::{PhotoParams, SendPhotoOperation},
+                poll::{PollParams, SendPollOperation},
+                video::{SendVideoOperation, VideoParams},
+                MessageFormat, SendOperation, SendParams,
+            },
+            BotParams,
+        },
+        OperationError, RootParams,
+    },
+};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+pub(crate) mod logging;
+pub(crate) mod validators;
+
+#[derive(Parser)]
+pub struct Cli {
+    /// How much verbose the output should be.
+    #[clap(short, long = "verbose", max_occurrences = 4, parse(from_occurrences))]
+    verbosity: u8,
+    #[clap(subcommand)]
+    subcommands: CliSubcommands,
+}
+
+#[derive(Subcommand)]
+enum CliSubcommands {
+    /// The operation for bots.
+    Bot {
+        /// The token of bot.
+        #[clap(short, long, env = "TELEGRAM_BOT_TOKEN")]
+        token: String,
+        #[clap(subcommand)]
+        subcommands: BotSubcommands,
+    },
+}
+
+#[derive(Subcommand)]
+enum BotSubcommands {
+    /// Send various types of messages.
+    Send {
+        /// The user id of receiver.
+        #[clap(short, long)]
+        receiver: String,
+        /// The format of message.
+        #[clap(long, arg_enum, default_value = "markdown")]
+        format: MessageFormat,
+        /// Whether to send notification to user.
+        #[clap(long)]
+        silent: bool,
+        /// Do not let user to forward or save message.
+        #[clap(long)]
+        protect_content: bool,
+        #[clap(subcommand)]
+        subcommands: SendSubcommands,
+    },
+}
+
+#[derive(Subcommand)]
+pub(crate) enum SendSubcommands {
+    /// A regular text message.
+    Message { message: String },
+    /// A file.
+    Document {
+        /// The file to be sent.
+        #[clap(validator = file_presence_validator)]
+        file: path::PathBuf,
+        /// The file caption.
+        #[clap(short, long, validator = caption_validator)]
+        message: Option<String>,
+        /// A preview image for file.
+        #[clap(long, validator = file_presence_validator)]
+        thumbnail: Option<path::PathBuf>,
+    },
+    /// A message with image viewer.
+    Photo {
+        /// The file to be sent.
+        #[clap(validator = image_validator)]
+        file: path::PathBuf,
+        /// The file caption.
+        #[clap(short, long, validator = caption_validator)]
+        message: Option<String>,
+    },
+    /// A message with video viewer.
+    Video {
+        /// The file to be sent.
+        #[clap(validator = video_validator)]
+        file: path::PathBuf,
+        /// The file caption.
+        #[clap(short, long, validator = caption_validator)]
+        message: Option<String>,
+    },
+    /// A message with audio player.
+    Audio {
+        /// The file to be sent.
+        #[clap(validator = audio_validator)]
+        file: path::PathBuf,
+        /// The file caption.
+        #[clap(short, long, validator = caption_validator)]
+        message: Option<String>,
+        #[clap(long)]
+        performer: Option<String>,
+        #[clap(long)]
+        title: Option<String>,
+    },
+    /// A message with question and options.
+    Poll {
+        /// Question.
+        #[clap(validator = poll_question_validator)]
+        question: String,
+        /// One of options.
+        #[clap(short = 'o', long = "option", multiple_occurrences = true, min_values = 2, validator = poll_option_validator)]
+        options: Vec<String>,
+    },
+    /// A location on earth.
+    Location {
+        /// The latitude of location.
+        #[clap(short = 'x', long)]
+        latitude: f32,
+        /// The longitude of location.
+        #[clap(short = 'y', long)]
+        longitude: f32,
+    },
+}
+
+pub fn match_app(cli: Cli) -> Result<(), OperationError> {
+    set_logger(cli.verbosity)
+        .map_err(|set_logger_error| OperationError::LoggerError(set_logger_error))?;
+
+    let root_params = RootParams::new(cli.verbosity);
+
+    match cli.subcommands {
+        CliSubcommands::Bot { token, subcommands } => {
+            let bot_params = BotParams::new(token);
+
+            match subcommands {
+                BotSubcommands::Send {
+                    receiver,
+                    format,
+                    silent,
+                    protect_content,
+                    subcommands,
+                } => {
+                    let send_params = SendParams::new(receiver, format, silent, protect_content);
+
+                    match subcommands {
+                        SendSubcommands::Message { message } => {
+                            let message_params = MessageParams::new(message);
+                            let send_message_operation = SendMessageOperation::new((
+                                root_params,
+                                bot_params,
+                                send_params,
+                                message_params,
+                            ));
+                            send_message_operation.send()
+                        }
+                        SendSubcommands::Document {
+                            file,
+                            message,
+                            thumbnail,
+                        } => {
+                            let document_params = DocumentParams::new(file, thumbnail, message);
+                            let send_document_operation = SendDocumentOperation::new((
+                                root_params,
+                                bot_params,
+                                send_params,
+                                document_params,
+                            ));
+                            send_document_operation.send()
+                        }
+                        SendSubcommands::Photo { file, message } => {
+                            let photo_params = PhotoParams::new(file, message);
+                            let send_photo_operation = SendPhotoOperation::new((
+                                root_params,
+                                bot_params,
+                                send_params,
+                                photo_params,
+                            ));
+                            send_photo_operation.send()
+                        }
+                        SendSubcommands::Video { file, message } => {
+                            let video_params = VideoParams::new(file, message);
+                            let send_video_operation = SendVideoOperation::new((
+                                root_params,
+                                bot_params,
+                                send_params,
+                                video_params,
+                            ));
+                            send_video_operation.send()
+                        }
+                        SendSubcommands::Audio {
+                            file,
+                            message,
+                            performer,
+                            title,
+                        } => {
+                            let audio_params = AudioParams::new(file, message, title, performer);
+                            let send_audio_operation = SendAudioOperation::new((
+                                root_params,
+                                bot_params,
+                                send_params,
+                                audio_params,
+                            ));
+                            send_audio_operation.send()
+                        }
+                        SendSubcommands::Poll { question, options } => {
+                            let poll_params = PollParams::new(question, options);
+                            let send_poll_operation = SendPollOperation::new((
+                                root_params,
+                                bot_params,
+                                send_params,
+                                poll_params,
+                            ));
+                            send_poll_operation.send()
+                        }
+                        SendSubcommands::Location {
+                            latitude,
+                            longitude,
+                        } => {
+                            let location_params = LocationParams::new(latitude, longitude);
+                            let send_location_operation = SendLocationOperation::new((
+                                root_params,
+                                bot_params,
+                                send_params,
+                                location_params,
+                            ));
+                            send_location_operation.send()
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/cli/validators.rs b/src/cli/validators.rs
new file mode 100644
index 0000000..2416087
--- /dev/null
+++ b/src/cli/validators.rs
@@ -0,0 +1,84 @@
+use std::path;
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+type ValidatorResult<T> = Result<T, String>;
+
+pub(crate) fn caption_validator(value: &str) -> ValidatorResult<String> {
+    if value.chars().count() > 1024 {
+        return Err(String::from(
+            "Message cannot be larger than 1024 characters.",
+        ));
+    }
+
+    Ok(value.to_owned())
+}
+
+pub(crate) fn poll_question_validator(value: &str) -> ValidatorResult<String> {
+    match value.len() {
+        l if l < 1 || l > 300 => Err("The question length must be between 1 and 300.".to_owned()),
+        _ => Ok(value.to_owned()),
+    }
+}
+
+pub(crate) fn poll_option_validator(value: &str) -> ValidatorResult<String> {
+    match value.len() {
+        l if l < 1 || l > 100 => {
+            return Err("The option length must be between 1 and 100.".to_owned())
+        }
+        _ => Ok(value.to_owned()),
+    }
+}
+
+pub(crate) fn file_presence_validator(value: &str) -> ValidatorResult<path::PathBuf> {
+    let path = path::PathBuf::from(value);
+
+    if path.is_file() {
+        Ok(path)
+    } else {
+        Err(format!("`{}` does not exist or is not a file.", value))
+    }
+}
+
+pub(crate) fn image_validator(value: &str) -> ValidatorResult<path::PathBuf> {
+    let path = file_presence_validator(value)?;
+    let mimetype = mime_guess::from_path(&path)
+        .first()
+        .ok_or_else(|| "File type could not be recognized.".to_owned())?;
+
+    if mimetype.type_() == mime::IMAGE {
+        Ok(path)
+    } else {
+        Err("Provided file is not a valid image file.".to_owned())
+    }
+}
+
+pub(crate) fn video_validator(value: &str) -> ValidatorResult<path::PathBuf> {
+    let path = file_presence_validator(value)?;
+    let mimetype = mime_guess::from_path(&path)
+        .first()
+        .ok_or_else(|| "File type could not be recognized.".to_owned())?;
+
+    if mimetype.type_() == mime::VIDEO {
+        Ok(path)
+    } else {
+        Err("Provided file is not a valid video file.".to_owned())
+    }
+}
+
+pub(crate) fn audio_validator(value: &str) -> ValidatorResult<path::PathBuf> {
+    let path = file_presence_validator(value)?;
+    let mimetype = mime_guess::from_path(&path)
+        .first()
+        .ok_or_else(|| "File type could not be recognized.")?;
+
+    if mimetype.type_() == mime::AUDIO {
+        Ok(path)
+    } else {
+        Err("Provided file is not a valid audio file.".to_owned())
+    }
+}
diff --git a/src/http/mod.rs b/src/http/mod.rs
new file mode 100644
index 0000000..e305353
--- /dev/null
+++ b/src/http/mod.rs
@@ -0,0 +1,8 @@
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+pub(crate) mod request;
+pub(crate) mod response;
diff --git a/src/http/request/mod.rs b/src/http/request/mod.rs
new file mode 100644
index 0000000..6ef8b8f
--- /dev/null
+++ b/src/http/request/mod.rs
@@ -0,0 +1,7 @@
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+pub(crate) mod models;
diff --git a/src/http/request/models/mod.rs b/src/http/request/models/mod.rs
new file mode 100644
index 0000000..6b9781b
--- /dev/null
+++ b/src/http/request/models/mod.rs
@@ -0,0 +1,60 @@
+use std::{path, string};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+pub(crate) mod sendaudio;
+pub(crate) mod senddocument;
+pub(crate) mod sendlocation;
+pub(crate) mod sendmessage;
+pub(crate) mod sendphoto;
+pub(crate) mod sendpoll;
+pub(crate) mod sendvideo;
+
+#[derive(Debug)]
+/// What the type of ChatId is.
+enum ChatId {
+    Int(usize),
+    Str(String),
+}
+
+impl string::ToString for ChatId {
+    fn to_string(&self) -> String {
+        match self {
+            ChatId::Int(v) => v.to_string(),
+            ChatId::Str(v) => v.clone(),
+        }
+    }
+}
+
+#[derive(Debug)]
+/// Which format Telegram should handle the message text in.
+enum ParseMode {
+    Markdown,
+    HTML,
+}
+
+impl string::ToString for ParseMode {
+    fn to_string(&self) -> String {
+        match self {
+            ParseMode::Markdown => "MarkdownV2".to_owned(),
+            ParseMode::HTML => "HTML".to_owned(),
+        }
+    }
+}
+
+// TODO remote and id will be implemented
+#[allow(dead_code)]
+#[derive(Debug)]
+/// The file that will be sent to Telegram.
+enum InputFile {
+    /// Local file.
+    Local(path::PathBuf),
+    /// Remote file URL.
+    Remote(url::Url),
+    /// The id of file that was sent before.
+    Id(String),
+}
diff --git a/src/http/request/models/sendaudio.rs b/src/http/request/models/sendaudio.rs
new file mode 100644
index 0000000..f28c896
--- /dev/null
+++ b/src/http/request/models/sendaudio.rs
@@ -0,0 +1,113 @@
+use std::convert::TryFrom;
+
+use reqwest::blocking::multipart::Form;
+
+use crate::operations::{
+    bot::send::{self, audio::SendAudioParams},
+    OperationError,
+};
+
+use super::{ChatId, InputFile, ParseMode};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[derive(Debug)]
+pub(crate) struct SendAudioRequestModel {
+    chat_id: ChatId,
+    audio: InputFile,
+    performer: Option<String>,
+    title: Option<String>,
+    caption: Option<String>,
+    parse_mode: ParseMode,
+    disable_notification: bool,
+    protect_content: bool,
+}
+
+impl TryFrom<SendAudioRequestModel> for Form {
+    type Error = OperationError;
+
+    fn try_from(m: SendAudioRequestModel) -> Result<Self, Self::Error> {
+        debug!("Converting SendAudioRequestModel to Form...");
+
+        let chat_id = m.chat_id.to_string();
+        let parse_mode = m.parse_mode.to_string();
+
+        let initial_form = Form::new()
+            .text("chat_id", chat_id)
+            .text("parse_mode", parse_mode);
+
+        let caption_form = match m.caption {
+            Some(c) => initial_form.text("caption", c),
+            None => initial_form,
+        };
+
+        let audio_form = match m.audio {
+            InputFile::Local(p) => caption_form
+                .file("audio", p)
+                .map_err(|err| OperationError::IoError(err))?,
+            InputFile::Remote(u) => caption_form.text("audio", u.to_string()),
+            InputFile::Id(i) => caption_form.text("audio", i),
+        };
+
+        let performer_form = match m.performer {
+            Some(p) => audio_form.text("performer", p),
+            None => audio_form,
+        };
+
+        let title_form = match m.title {
+            Some(t) => performer_form.text("title", t),
+            None => performer_form,
+        };
+
+        let notification_form = match m.disable_notification {
+            true => title_form.text("disable_notification", "true"),
+            false => title_form,
+        };
+
+        let protect_content_form = match m.protect_content {
+            true => notification_form.text("protect_content", "true"),
+            false => notification_form,
+        };
+
+        Ok(protect_content_form)
+    }
+}
+
+impl From<SendAudioParams> for SendAudioRequestModel {
+    fn from(params: SendAudioParams) -> Self {
+        debug!("Converting SendAudioParams to SendAudioRequestModel...");
+
+        let chat_id = match params.2.receiver.parse::<usize>() {
+            Ok(v) => ChatId::Int(v),
+            Err(_) => ChatId::Str(params.2.receiver),
+        };
+
+        let caption = params.3.message;
+
+        let parse_mode = match params.2.format {
+            send::MessageFormat::Markdown => ParseMode::Markdown,
+            send::MessageFormat::HTML => ParseMode::HTML,
+        };
+
+        let audio = InputFile::Local(params.3.file);
+        let performer = params.3.performer;
+        let title = params.3.title;
+        let disable_notification = params.2.silent;
+        let protect_content = params.2.protect_content;
+
+        SendAudioRequestModel {
+            chat_id,
+            caption,
+            parse_mode,
+            audio,
+            performer,
+            title,
+            disable_notification,
+            protect_content,
+        }
+    }
+}
diff --git a/src/http/request/models/senddocument.rs b/src/http/request/models/senddocument.rs
new file mode 100644
index 0000000..73bcfeb
--- /dev/null
+++ b/src/http/request/models/senddocument.rs
@@ -0,0 +1,116 @@
+use std::convert::TryFrom;
+
+use reqwest::blocking::multipart::Form;
+
+use crate::operations::{
+    bot::send::{self, document::SendDocumentParams},
+    OperationError,
+};
+
+use super::{ChatId, InputFile, ParseMode};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[derive(Debug)]
+/// A model for /sendDocument request.
+pub(crate) struct SendDocumentRequestModel {
+    chat_id: ChatId,
+    document: InputFile,
+    thumbnail: Option<InputFile>,
+    caption: Option<String>,
+    parse_mode: ParseMode,
+    disable_notification: bool,
+    protect_content: bool,
+}
+
+impl TryFrom<SendDocumentRequestModel> for Form {
+    type Error = OperationError;
+
+    fn try_from(m: SendDocumentRequestModel) -> Result<Self, Self::Error> {
+        debug!("Converting SendDocumentRequestModel to Form...");
+        let chat_id = m.chat_id.to_string();
+        let parse_mode = m.parse_mode.to_string();
+
+        let initial_form = Form::new()
+            .text("chat_id", chat_id)
+            .text("parse_mode", parse_mode);
+
+        let caption_form = match m.caption {
+            Some(c) => initial_form.text("caption", c),
+            None => initial_form,
+        };
+
+        let document_form = match m.document {
+            InputFile::Local(p) => caption_form
+                .file("document", p)
+                .map_err(|err| OperationError::IoError(err))?,
+            InputFile::Remote(u) => caption_form.text("document", u.to_string()),
+            InputFile::Id(i) => caption_form.text("document", i),
+        };
+
+        let thumbnail_form = match m.thumbnail {
+            Some(inputfile) => match inputfile {
+                InputFile::Local(p) => document_form
+                    .file("thumbnail", p)
+                    .map_err(|err| OperationError::IoError(err))?,
+                InputFile::Remote(u) => document_form.text("thumbnail", u.to_string()),
+                InputFile::Id(i) => document_form.text("thumbnail", i),
+            },
+            None => document_form,
+        };
+
+        let notification_form = match m.disable_notification {
+            true => thumbnail_form.text("disable_notification", "true"),
+            false => thumbnail_form,
+        };
+
+        let protect_content_form = match m.protect_content {
+            true => notification_form.text("protect_content", "true"),
+            false => notification_form,
+        };
+
+        Ok(protect_content_form)
+    }
+}
+
+impl From<SendDocumentParams> for SendDocumentRequestModel {
+    fn from(params: SendDocumentParams) -> Self {
+        debug!("Converting SendDocumentParams to SendDocumentRequestModel...");
+
+        let chat_id = match params.2.receiver.parse::<usize>() {
+            Ok(v) => ChatId::Int(v),
+            Err(_) => ChatId::Str(params.2.receiver),
+        };
+
+        let caption = params.3.message;
+
+        let parse_mode = match params.2.format {
+            send::MessageFormat::Markdown => ParseMode::Markdown,
+            send::MessageFormat::HTML => ParseMode::HTML,
+        };
+
+        let document = InputFile::Local(params.3.file);
+
+        let thumbnail = match params.3.thumbnail {
+            Some(p) => Some(InputFile::Local(p)),
+            None => None,
+        };
+
+        let disable_notification = params.2.silent;
+        let protect_content = params.2.protect_content;
+
+        SendDocumentRequestModel {
+            chat_id,
+            caption,
+            parse_mode,
+            document,
+            thumbnail,
+            disable_notification,
+            protect_content,
+        }
+    }
+}
diff --git a/src/http/request/models/sendlocation.rs b/src/http/request/models/sendlocation.rs
new file mode 100644
index 0000000..1aac9a9
--- /dev/null
+++ b/src/http/request/models/sendlocation.rs
@@ -0,0 +1,71 @@
+use std::convert::TryFrom;
+
+use reqwest::blocking::multipart::Form;
+
+use crate::operations::{bot::send::location::SendLocationParams, OperationError};
+
+use super::ChatId;
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[derive(Debug)]
+pub(crate) struct SendLocationRequestModel {
+    chat_id: ChatId,
+    latitude: f32,
+    longitude: f32,
+    disable_notification: bool,
+    protect_content: bool,
+}
+
+impl TryFrom<SendLocationRequestModel> for Form {
+    type Error = OperationError;
+
+    fn try_from(m: SendLocationRequestModel) -> Result<Self, Self::Error> {
+        debug!("Converting SendLocationRequestModel to Form...");
+        let chat_id = m.chat_id.to_string();
+
+        let initial_form = Form::new()
+            .text("chat_id", chat_id)
+            .text("latitude", m.latitude.to_string())
+            .text("longitude", m.longitude.to_string());
+
+        let notification_form = match m.disable_notification {
+            true => initial_form.text("disable_notification", "true"),
+            false => initial_form,
+        };
+        let protect_content_form = match m.protect_content {
+            true => notification_form.text("protect_content", "true"),
+            false => notification_form,
+        };
+
+        Ok(protect_content_form)
+    }
+}
+
+impl From<SendLocationParams> for SendLocationRequestModel {
+    fn from(params: SendLocationParams) -> Self {
+        debug!("Converting SendLocationParams to SendLocationRequestModel...");
+
+        let chat_id = match params.2.receiver.parse::<usize>() {
+            Ok(v) => ChatId::Int(v),
+            Err(_) => ChatId::Str(params.2.receiver),
+        };
+
+        let latitude = params.3.latitude;
+        let longitude = params.3.longitude;
+        let disable_notification = params.2.silent;
+        let protect_content = params.2.protect_content;
+
+        SendLocationRequestModel {
+            chat_id,
+            latitude,
+            longitude,
+            disable_notification,
+            protect_content,
+        }
+    }
+}
diff --git a/src/http/request/models/sendmessage.rs b/src/http/request/models/sendmessage.rs
new file mode 100644
index 0000000..6570b50
--- /dev/null
+++ b/src/http/request/models/sendmessage.rs
@@ -0,0 +1,82 @@
+use std::convert::TryFrom;
+
+use reqwest::blocking::multipart::Form;
+
+use crate::operations::{
+    bot::send::{self, message::SendMessageParams},
+    OperationError,
+};
+
+use super::{ChatId, ParseMode};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[derive(Debug)]
+/// A model for /sendMessage request.
+pub(crate) struct SendMessageRequestModel {
+    chat_id: ChatId,
+    text: String,
+    parse_mode: ParseMode,
+    disable_notification: bool,
+    protect_content: bool,
+}
+
+impl TryFrom<SendMessageRequestModel> for Form {
+    type Error = OperationError;
+
+    fn try_from(m: SendMessageRequestModel) -> Result<Self, Self::Error> {
+        debug!("Converting SendMessageRequestModel to Form...");
+        let chat_id = m.chat_id.to_string();
+        let parse_mode = m.parse_mode.to_string();
+
+        let initial_form = Form::new()
+            .text("chat_id", chat_id)
+            .text("text", m.text)
+            .text("parse_mode", parse_mode);
+
+        let notification_form = match m.disable_notification {
+            true => initial_form.text("disable_notification", "true"),
+            false => initial_form,
+        };
+
+        let protect_content_form = match m.protect_content {
+            true => notification_form.text("protect_content", "true"),
+            false => notification_form,
+        };
+
+        Ok(protect_content_form)
+    }
+}
+
+impl From<SendMessageParams> for SendMessageRequestModel {
+    fn from(params: SendMessageParams) -> Self {
+        debug!("Converting SendMessageParams to SendMessageRequestModel...");
+
+        let chat_id = match params.2.receiver.parse::<usize>() {
+            Ok(v) => ChatId::Int(v),
+            Err(_) => ChatId::Str(params.2.receiver),
+        };
+
+        let text = params.3.message;
+
+        let parse_mode = match params.2.format {
+            send::MessageFormat::Markdown => ParseMode::Markdown,
+            send::MessageFormat::HTML => ParseMode::HTML,
+        };
+
+        let disable_notification = params.2.silent;
+        let protect_content = params.2.protect_content;
+
+        SendMessageRequestModel {
+            chat_id,
+            text,
+            parse_mode,
+            disable_notification,
+            protect_content,
+        }
+    }
+}
diff --git a/src/http/request/models/sendphoto.rs b/src/http/request/models/sendphoto.rs
new file mode 100644
index 0000000..2b2b922
--- /dev/null
+++ b/src/http/request/models/sendphoto.rs
@@ -0,0 +1,95 @@
+use std::convert::TryFrom;
+
+use reqwest::blocking::multipart::Form;
+
+use crate::operations::{
+    bot::send::{self, photo::SendPhotoParams},
+    OperationError,
+};
+
+use super::{ChatId, InputFile, ParseMode};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[derive(Debug)]
+pub(crate) struct SendPhotoRequestModel {
+    chat_id: ChatId,
+    photo: InputFile,
+    caption: Option<String>,
+    parse_mode: ParseMode,
+    disable_notification: bool,
+    protect_content: bool,
+}
+
+impl TryFrom<SendPhotoRequestModel> for Form {
+    type Error = OperationError;
+
+    fn try_from(m: SendPhotoRequestModel) -> Result<Self, Self::Error> {
+        debug!("Converting SendPhotoRequestModel to Form...");
+        let chat_id = m.chat_id.to_string();
+        let parse_mode = m.parse_mode.to_string();
+
+        let initial_form = Form::new()
+            .text("chat_id", chat_id)
+            .text("parse_mode", parse_mode);
+
+        let caption_form = match m.caption {
+            Some(c) => initial_form.text("caption", c),
+            None => initial_form,
+        };
+
+        let photo_form = match m.photo {
+            InputFile::Local(p) => caption_form
+                .file("photo", p)
+                .map_err(|err| OperationError::IoError(err))?,
+            InputFile::Remote(u) => caption_form.text("photo", u.to_string()),
+            InputFile::Id(i) => caption_form.text("photo", i),
+        };
+
+        let notification_form = match m.disable_notification {
+            true => photo_form.text("disable_notification", "true"),
+            false => photo_form,
+        };
+        let protect_content_form = match m.protect_content {
+            true => notification_form.text("protect_content", "true"),
+            false => notification_form,
+        };
+
+        Ok(protect_content_form)
+    }
+}
+
+impl From<SendPhotoParams> for SendPhotoRequestModel {
+    fn from(params: SendPhotoParams) -> Self {
+        debug!("Converting SendPhotoParams to SendPhotoRequestModel...");
+
+        let chat_id = match params.2.receiver.parse::<usize>() {
+            Ok(v) => ChatId::Int(v),
+            Err(_) => ChatId::Str(params.2.receiver),
+        };
+
+        let caption = params.3.message;
+
+        let parse_mode = match params.2.format {
+            send::MessageFormat::Markdown => ParseMode::Markdown,
+            send::MessageFormat::HTML => ParseMode::HTML,
+        };
+
+        let photo = InputFile::Local(params.3.file);
+        let disable_notification = params.2.silent;
+        let protect_content = params.2.protect_content;
+
+        SendPhotoRequestModel {
+            chat_id,
+            caption,
+            parse_mode,
+            photo,
+            disable_notification,
+            protect_content,
+        }
+    }
+}
diff --git a/src/http/request/models/sendpoll.rs b/src/http/request/models/sendpoll.rs
new file mode 100644
index 0000000..64ccc8d
--- /dev/null
+++ b/src/http/request/models/sendpoll.rs
@@ -0,0 +1,74 @@
+use std::convert::TryFrom;
+
+use reqwest::blocking::multipart::Form;
+use serde_json::json;
+
+use crate::operations::{bot::send::poll::SendPollParams, OperationError};
+
+use super::ChatId;
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[derive(Debug)]
+pub(crate) struct SendPollRequestModel {
+    chat_id: ChatId,
+    question: String,
+    options: Vec<String>,
+    disable_notification: bool,
+    protect_content: bool,
+}
+
+impl TryFrom<SendPollRequestModel> for Form {
+    type Error = OperationError;
+
+    fn try_from(m: SendPollRequestModel) -> Result<Self, Self::Error> {
+        debug!("Converting SendPollRequestModel to Form...");
+        let chat_id = m.chat_id.to_string();
+        let options = json!(m.options).to_string();
+
+        let initial_form = Form::new()
+            .text("chat_id", chat_id)
+            .text("question", m.question)
+            .text("options", options);
+
+        let notification_form = match m.disable_notification {
+            true => initial_form.text("disable_notification", "true"),
+            false => initial_form,
+        };
+
+        let protect_content_form = match m.protect_content {
+            true => notification_form.text("protect_content", "true"),
+            false => notification_form,
+        };
+
+        Ok(protect_content_form)
+    }
+}
+
+impl From<SendPollParams> for SendPollRequestModel {
+    fn from(params: SendPollParams) -> Self {
+        debug!("Converting SendPollParams to SendPollRequestModel...");
+
+        let chat_id = match params.2.receiver.parse::<usize>() {
+            Ok(v) => ChatId::Int(v),
+            Err(_) => ChatId::Str(params.2.receiver),
+        };
+
+        let question = params.3.question;
+        let options = params.3.options;
+        let disable_notification = params.2.silent;
+        let protect_content = params.2.protect_content;
+
+        SendPollRequestModel {
+            chat_id,
+            question,
+            options,
+            disable_notification,
+            protect_content,
+        }
+    }
+}
diff --git a/src/http/request/models/sendvideo.rs b/src/http/request/models/sendvideo.rs
new file mode 100644
index 0000000..a9be62a
--- /dev/null
+++ b/src/http/request/models/sendvideo.rs
@@ -0,0 +1,97 @@
+use std::convert::TryFrom;
+
+use reqwest::blocking::multipart::Form;
+
+use crate::operations::{
+    bot::send::{self, video::SendVideoParams},
+    OperationError,
+};
+
+use super::{ChatId, InputFile, ParseMode};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[derive(Debug)]
+pub(crate) struct SendVideoRequestModel {
+    chat_id: ChatId,
+    video: InputFile,
+    caption: Option<String>,
+    parse_mode: ParseMode,
+    disable_notification: bool,
+    protect_content: bool,
+}
+
+impl TryFrom<SendVideoRequestModel> for Form {
+    type Error = OperationError;
+
+    fn try_from(m: SendVideoRequestModel) -> Result<Self, Self::Error> {
+        debug!("Converting to SendVideoRequestModel to Form...");
+        let chat_id = m.chat_id.to_string();
+        let parse_mode = m.parse_mode.to_string();
+
+        let initial_form = Form::new()
+            .text("chat_id", chat_id)
+            .text("parse_mode", parse_mode);
+
+        let caption_form = match m.caption {
+            Some(c) => initial_form.text("caption", c),
+            None => initial_form,
+        };
+
+        let video_form = match m.video {
+            InputFile::Local(p) => caption_form
+                .file("video", p)
+                .map_err(|err| OperationError::IoError(err))?,
+            InputFile::Remote(u) => caption_form.text("video", u.to_string()),
+            InputFile::Id(i) => caption_form.text("video", i),
+        };
+
+        let notification_form = match m.disable_notification {
+            true => video_form.text("disable_notification", "true"),
+            false => video_form,
+        };
+
+        let protect_content_form = match m.protect_content {
+            true => notification_form.text("protect_content", "true"),
+            false => notification_form,
+        };
+
+        Ok(protect_content_form)
+    }
+}
+
+impl From<SendVideoParams> for SendVideoRequestModel {
+    fn from(params: SendVideoParams) -> Self {
+        debug!("Converting SendVideoParams to SendVideoRequestModel...");
+
+        let chat_id = match params.2.receiver.parse::<usize>() {
+            Ok(v) => ChatId::Int(v),
+            Err(_) => ChatId::Str(params.2.receiver),
+        };
+
+        let caption = params.3.message;
+
+        let parse_mode = match params.2.format {
+            send::MessageFormat::Markdown => ParseMode::Markdown,
+            send::MessageFormat::HTML => ParseMode::HTML,
+        };
+
+        let video = InputFile::Local(params.3.file);
+
+        let disable_notification = params.2.silent;
+        let protect_content = params.2.protect_content;
+
+        SendVideoRequestModel {
+            chat_id,
+            caption,
+            parse_mode,
+            video,
+            disable_notification,
+            protect_content,
+        }
+    }
+}
diff --git a/src/http/response/mod.rs b/src/http/response/mod.rs
new file mode 100644
index 0000000..6ef8b8f
--- /dev/null
+++ b/src/http/response/mod.rs
@@ -0,0 +1,7 @@
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+pub(crate) mod models;
diff --git a/src/http/response/models/message.rs b/src/http/response/models/message.rs
new file mode 100644
index 0000000..3eb693a
--- /dev/null
+++ b/src/http/response/models/message.rs
@@ -0,0 +1,8 @@
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[derive(Debug, Deserialize)]
+pub(crate) struct MessageModel;
diff --git a/src/http/response/models/mod.rs b/src/http/response/models/mod.rs
new file mode 100644
index 0000000..fcd542c
--- /dev/null
+++ b/src/http/response/models/mod.rs
@@ -0,0 +1,14 @@
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+pub(crate) mod message;
+
+#[derive(Debug, Deserialize)]
+pub(crate) struct GenericResponseModel {
+    // ok: bool,
+    // error_code: Option<usize>,
+    pub(crate) description: Option<String>,
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..1f0aabc
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,20 @@
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[macro_use]
+extern crate log;
+
+#[macro_use]
+extern crate serde;
+
+#[macro_use]
+extern crate clap;
+
+pub mod cli;
+mod http;
+pub(crate) mod operations;
+
+const API_ROOT_URL: &str = "https://api.telegram.org/bot";
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..c1f945f
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,22 @@
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+use std::process;
+
+use clap::StructOpt;
+use log::error;
+use tgcli::cli::{match_app, Cli};
+
+fn main() {
+    let cli = Cli::parse();
+    match match_app(cli) {
+        Ok(_) => (),
+        Err(e) => {
+            error!("{}", e);
+            process::exit(e.exit_code());
+        }
+    }
+}
diff --git a/src/operations/bot/mod.rs b/src/operations/bot/mod.rs
new file mode 100644
index 0000000..b7779ee
--- /dev/null
+++ b/src/operations/bot/mod.rs
@@ -0,0 +1,18 @@
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+pub(crate) mod send;
+
+#[derive(Debug)]
+pub(crate) struct BotParams {
+    token: String,
+}
+
+impl BotParams {
+    pub(crate) fn new(token: String) -> Self {
+        Self { token }
+    }
+}
diff --git a/src/operations/bot/send/audio.rs b/src/operations/bot/send/audio.rs
new file mode 100644
index 0000000..4f25fab
--- /dev/null
+++ b/src/operations/bot/send/audio.rs
@@ -0,0 +1,93 @@
+use std::{convert::TryInto, path::PathBuf};
+
+use reqwest::blocking::Client;
+
+use crate::{
+    http::{
+        request::models::sendaudio::SendAudioRequestModel, response::models::GenericResponseModel,
+    },
+    operations::{bot::BotParams, OperationError, RootParams},
+    API_ROOT_URL,
+};
+
+use super::{SendOperation, SendParams};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[derive(Debug)]
+pub(crate) struct AudioParams {
+    pub(crate) file: PathBuf,
+    pub(crate) message: Option<String>,
+    pub(crate) title: Option<String>,
+    pub(crate) performer: Option<String>,
+}
+
+impl AudioParams {
+    pub(crate) fn new(
+        file: PathBuf,
+        message: Option<String>,
+        title: Option<String>,
+        performer: Option<String>,
+    ) -> Self {
+        Self {
+            file,
+            message,
+            title,
+            performer,
+        }
+    }
+}
+
+pub(crate) type SendAudioParams = (RootParams, BotParams, SendParams, AudioParams);
+
+#[derive(Debug)]
+pub(crate) struct SendAudioOperation {
+    params: SendAudioParams,
+}
+
+impl SendAudioOperation {
+    pub(crate) fn new(params: SendAudioParams) -> Self {
+        Self { params }
+    }
+}
+
+impl SendOperation for SendAudioOperation {
+    fn send(self) -> Result<(), crate::operations::OperationError> {
+        info!("🔊 Sending audio...");
+
+        let url = format!(
+            "{root_url}{token}/sendAudio",
+            root_url = API_ROOT_URL,
+            token = self.params.1.token
+        );
+        trace!("url: {}", url);
+
+        let req_instance: SendAudioRequestModel = self.params.into();
+        let req_body = match req_instance.try_into() {
+            Ok(f) => f,
+            Err(e) => return Err(e),
+        };
+        debug!("request body: {:?}", req_body);
+
+        // TODO add logging to response and data
+        let client = Client::new();
+        let response = client
+            .post(url)
+            .multipart(req_body)
+            .send()
+            .map_err(|err| OperationError::ReqwestError(err))?;
+        let data = response
+            .json::<GenericResponseModel>()
+            .map_err(|err| OperationError::ReqwestError(err))?;
+
+        if let Some(description) = data.description {
+            return Err(OperationError::TelegramAPIError { description });
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/operations/bot/send/document.rs b/src/operations/bot/send/document.rs
new file mode 100644
index 0000000..14d36c1
--- /dev/null
+++ b/src/operations/bot/send/document.rs
@@ -0,0 +1,86 @@
+use std::{convert::TryInto, path::PathBuf};
+
+use reqwest::blocking::Client;
+
+use crate::{
+    http::{
+        request::models::senddocument::SendDocumentRequestModel,
+        response::models::GenericResponseModel,
+    },
+    operations::{bot::BotParams, OperationError, RootParams},
+    API_ROOT_URL,
+};
+
+use super::{SendOperation, SendParams};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[derive(Debug)]
+pub(crate) struct DocumentParams {
+    pub(crate) file: PathBuf,
+    pub(crate) thumbnail: Option<PathBuf>,
+    pub(crate) message: Option<String>,
+}
+
+impl DocumentParams {
+    pub(crate) fn new(file: PathBuf, thumbnail: Option<PathBuf>, message: Option<String>) -> Self {
+        Self {
+            file,
+            thumbnail,
+            message,
+        }
+    }
+}
+
+pub(crate) type SendDocumentParams = (RootParams, BotParams, SendParams, DocumentParams);
+
+#[derive(Debug)]
+pub(crate) struct SendDocumentOperation {
+    params: SendDocumentParams,
+}
+
+impl SendDocumentOperation {
+    pub(crate) fn new(params: SendDocumentParams) -> Self {
+        Self { params }
+    }
+}
+
+impl SendOperation for SendDocumentOperation {
+    fn send(self) -> Result<(), OperationError> {
+        info!("📎 Sending document...");
+
+        let url = format!(
+            "{root_url}{token}/sendDocument",
+            root_url = API_ROOT_URL,
+            token = self.params.1.token
+        );
+        trace!("url: {}", url);
+
+        let req_instance: SendDocumentRequestModel = self.params.into();
+        let req_body = match req_instance.try_into() {
+            Ok(f) => f,
+            Err(e) => return Err(e),
+        };
+        debug!("request body: {:?}", req_body);
+
+        let client = Client::new();
+        let response = client
+            .post(url)
+            .multipart(req_body)
+            .send()
+            .map_err(|err| OperationError::ReqwestError(err))?;
+        let data = response
+            .json::<GenericResponseModel>()
+            .map_err(|err| OperationError::ReqwestError(err))?;
+
+        if let Some(description) = data.description {
+            return Err(OperationError::TelegramAPIError { description });
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/operations/bot/send/location.rs b/src/operations/bot/send/location.rs
new file mode 100644
index 0000000..ef16cc0
--- /dev/null
+++ b/src/operations/bot/send/location.rs
@@ -0,0 +1,84 @@
+use std::convert::TryInto;
+
+use reqwest::blocking::Client;
+
+use crate::{
+    http::{
+        request::models::sendlocation::SendLocationRequestModel,
+        response::models::GenericResponseModel,
+    },
+    operations::{bot::BotParams, OperationError, RootParams},
+    API_ROOT_URL,
+};
+
+use super::{SendOperation, SendParams};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[derive(Debug)]
+pub(crate) struct LocationParams {
+    pub(crate) latitude: f32,
+    pub(crate) longitude: f32,
+}
+
+impl LocationParams {
+    pub(crate) fn new(latitude: f32, longitude: f32) -> Self {
+        Self {
+            latitude,
+            longitude,
+        }
+    }
+}
+
+pub(crate) type SendLocationParams = (RootParams, BotParams, SendParams, LocationParams);
+
+#[derive(Debug)]
+pub(crate) struct SendLocationOperation {
+    params: SendLocationParams,
+}
+
+impl SendLocationOperation {
+    pub(crate) fn new(params: SendLocationParams) -> Self {
+        Self { params }
+    }
+}
+
+impl SendOperation for SendLocationOperation {
+    fn send(self) -> Result<(), crate::operations::OperationError> {
+        info!("🌍 Sending location...");
+
+        let url = format!(
+            "{root_url}{token}/sendLocation",
+            root_url = API_ROOT_URL,
+            token = self.params.1.token,
+        );
+        trace!("url: {}", url);
+
+        let req_instance: SendLocationRequestModel = self.params.into();
+        let req_body = match req_instance.try_into() {
+            Ok(f) => f,
+            Err(e) => return Err(e),
+        };
+        trace!("request body: {:?}", req_body);
+
+        let client = Client::new();
+        let response = client
+            .post(url)
+            .multipart(req_body)
+            .send()
+            .map_err(|err| OperationError::ReqwestError(err))?;
+        let data = response
+            .json::<GenericResponseModel>()
+            .map_err(|err| OperationError::ReqwestError(err))?;
+
+        if let Some(description) = data.description {
+            return Err(OperationError::TelegramAPIError { description });
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/operations/bot/send/message.rs b/src/operations/bot/send/message.rs
new file mode 100644
index 0000000..efd7af3
--- /dev/null
+++ b/src/operations/bot/send/message.rs
@@ -0,0 +1,83 @@
+use std::convert::TryInto;
+
+use reqwest::blocking::Client;
+
+use crate::{
+    http::{
+        request::models::sendmessage::SendMessageRequestModel,
+        response::models::GenericResponseModel,
+    },
+    operations::{bot::BotParams, OperationError, RootParams},
+    API_ROOT_URL,
+};
+
+use super::{SendOperation, SendParams};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[derive(Debug)]
+pub(crate) struct MessageParams {
+    pub(crate) message: String,
+}
+
+impl MessageParams {
+    pub(crate) fn new(message: String) -> Self {
+        Self {
+            message: message.to_string(),
+        }
+    }
+}
+
+pub(crate) type SendMessageParams = (RootParams, BotParams, SendParams, MessageParams);
+
+#[derive(Debug)]
+pub(crate) struct SendMessageOperation {
+    params: SendMessageParams,
+}
+
+impl SendMessageOperation {
+    pub(crate) fn new(params: SendMessageParams) -> Self {
+        Self { params }
+    }
+}
+
+impl SendOperation for SendMessageOperation {
+    fn send(self) -> Result<(), OperationError> {
+        info!("✏️ Sending message...");
+
+        let url = format!(
+            "{root_url}{token}/sendMessage",
+            root_url = API_ROOT_URL,
+            token = self.params.1.token,
+        );
+        trace!("url: {}", url);
+
+        let req_instance: SendMessageRequestModel = self.params.into();
+        let req_body = match req_instance.try_into() {
+            Ok(f) => f,
+            Err(e) => return Err(e),
+        };
+        trace!("request body: {:?}", req_body);
+
+        // TODO set up client earlier on bot params
+        let client = Client::new();
+        let response = client
+            .post(url)
+            .multipart(req_body)
+            .send()
+            .map_err(|err| OperationError::ReqwestError(err))?;
+        let data = response
+            .json::<GenericResponseModel>()
+            .map_err(|err| OperationError::ReqwestError(err))?;
+
+        if let Some(description) = data.description {
+            return Err(OperationError::TelegramAPIError { description });
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/operations/bot/send/mod.rs b/src/operations/bot/send/mod.rs
new file mode 100644
index 0000000..a175a84
--- /dev/null
+++ b/src/operations/bot/send/mod.rs
@@ -0,0 +1,49 @@
+use crate::operations::OperationError;
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+pub(crate) mod audio;
+pub(crate) mod document;
+pub(crate) mod location;
+pub(crate) mod message;
+pub(crate) mod photo;
+pub(crate) mod poll;
+pub(crate) mod video;
+
+#[derive(Debug, Clone, ArgEnum)]
+pub(crate) enum MessageFormat {
+    Markdown,
+    HTML,
+}
+
+#[derive(Debug)]
+pub(crate) struct SendParams {
+    pub(crate) receiver: String,
+    pub(crate) format: MessageFormat,
+    pub(crate) silent: bool,
+    pub(crate) protect_content: bool,
+}
+
+impl SendParams {
+    pub(crate) fn new(
+        receiver: String,
+        format: MessageFormat,
+        silent: bool,
+        protect_content: bool,
+    ) -> Self {
+        Self {
+            receiver,
+            format,
+            silent,
+            protect_content,
+        }
+    }
+}
+
+pub(crate) trait SendOperation {
+    fn send(self) -> Result<(), OperationError>;
+}
diff --git a/src/operations/bot/send/photo.rs b/src/operations/bot/send/photo.rs
new file mode 100644
index 0000000..7399a70
--- /dev/null
+++ b/src/operations/bot/send/photo.rs
@@ -0,0 +1,80 @@
+use std::{convert::TryInto, path::PathBuf};
+
+use reqwest::blocking::Client;
+
+use crate::{
+    http::{
+        request::models::sendphoto::SendPhotoRequestModel, response::models::GenericResponseModel,
+    },
+    operations::{bot::BotParams, OperationError, RootParams},
+    API_ROOT_URL,
+};
+
+use super::{SendOperation, SendParams};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[derive(Debug)]
+pub(crate) struct PhotoParams {
+    pub(crate) file: PathBuf,
+    pub(crate) message: Option<String>,
+}
+
+impl PhotoParams {
+    pub(crate) fn new(file: PathBuf, message: Option<String>) -> Self {
+        Self { file, message }
+    }
+}
+
+pub(crate) type SendPhotoParams = (RootParams, BotParams, SendParams, PhotoParams);
+
+#[derive(Debug)]
+pub(crate) struct SendPhotoOperation {
+    params: SendPhotoParams,
+}
+
+impl SendPhotoOperation {
+    pub(crate) fn new(params: SendPhotoParams) -> Self {
+        Self { params }
+    }
+}
+
+impl SendOperation for SendPhotoOperation {
+    fn send(self) -> Result<(), OperationError> {
+        info!("📷 Sending photo...");
+
+        let url = format!(
+            "{root_url}{token}/sendPhoto",
+            root_url = API_ROOT_URL,
+            token = self.params.1.token
+        );
+        trace!("url: {}", url);
+
+        let req_instance: SendPhotoRequestModel = self.params.into();
+        let req_body = match req_instance.try_into() {
+            Ok(f) => f,
+            Err(e) => return Err(e),
+        };
+        debug!("request body: {:?}", req_body);
+
+        let client = Client::new();
+        let response = client
+            .post(url)
+            .multipart(req_body)
+            .send()
+            .map_err(|err| OperationError::ReqwestError(err))?;
+        let data = response
+            .json::<GenericResponseModel>()
+            .map_err(|err| OperationError::ReqwestError(err))?;
+
+        if let Some(description) = data.description {
+            return Err(OperationError::TelegramAPIError { description });
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/operations/bot/send/poll.rs b/src/operations/bot/send/poll.rs
new file mode 100644
index 0000000..64eede4
--- /dev/null
+++ b/src/operations/bot/send/poll.rs
@@ -0,0 +1,80 @@
+use std::convert::TryInto;
+
+use reqwest::blocking::Client;
+
+use crate::{
+    http::{
+        request::models::sendpoll::SendPollRequestModel, response::models::GenericResponseModel,
+    },
+    operations::{bot::BotParams, OperationError, RootParams},
+    API_ROOT_URL,
+};
+
+use super::{SendOperation, SendParams};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[derive(Debug)]
+pub(crate) struct PollParams {
+    pub(crate) question: String,
+    pub(crate) options: Vec<String>,
+}
+
+impl PollParams {
+    pub(crate) fn new(question: String, options: Vec<String>) -> Self {
+        Self { question, options }
+    }
+}
+
+pub(crate) type SendPollParams = (RootParams, BotParams, SendParams, PollParams);
+
+#[derive(Debug)]
+pub(crate) struct SendPollOperation {
+    params: SendPollParams,
+}
+
+impl SendPollOperation {
+    pub(crate) fn new(params: SendPollParams) -> Self {
+        Self { params }
+    }
+}
+
+impl SendOperation for SendPollOperation {
+    fn send(self) -> Result<(), crate::operations::OperationError> {
+        info!("🗯️ Sending poll...");
+
+        let url = format!(
+            "{root_url}{token}/sendPoll",
+            root_url = API_ROOT_URL,
+            token = self.params.1.token,
+        );
+        trace!("url: {}", url);
+
+        let req_instance: SendPollRequestModel = self.params.into();
+        let req_body = match req_instance.try_into() {
+            Ok(f) => f,
+            Err(e) => return Err(e),
+        };
+        trace!("request body: {:?}", req_body);
+
+        let client = Client::new();
+        let response = client
+            .post(url)
+            .multipart(req_body)
+            .send()
+            .map_err(|err| OperationError::ReqwestError(err))?;
+        let data = response
+            .json::<GenericResponseModel>()
+            .map_err(|err| OperationError::ReqwestError(err))?;
+
+        if let Some(description) = data.description {
+            return Err(OperationError::TelegramAPIError { description });
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/operations/bot/send/video.rs b/src/operations/bot/send/video.rs
new file mode 100644
index 0000000..7cde4de
--- /dev/null
+++ b/src/operations/bot/send/video.rs
@@ -0,0 +1,80 @@
+use std::{convert::TryInto, path::PathBuf};
+
+use reqwest::blocking::Client;
+
+use crate::{
+    http::{
+        request::models::sendvideo::SendVideoRequestModel, response::models::GenericResponseModel,
+    },
+    operations::{bot::BotParams, OperationError, RootParams},
+    API_ROOT_URL,
+};
+
+use super::{SendOperation, SendParams};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[derive(Debug)]
+pub(crate) struct VideoParams {
+    pub(crate) file: PathBuf,
+    pub(crate) message: Option<String>,
+}
+
+impl VideoParams {
+    pub(crate) fn new(file: PathBuf, message: Option<String>) -> Self {
+        Self { file, message }
+    }
+}
+
+pub(crate) type SendVideoParams = (RootParams, BotParams, SendParams, VideoParams);
+
+#[derive(Debug)]
+pub(crate) struct SendVideoOperation {
+    params: SendVideoParams,
+}
+
+impl SendVideoOperation {
+    pub(crate) fn new(params: SendVideoParams) -> Self {
+        Self { params }
+    }
+}
+
+impl SendOperation for SendVideoOperation {
+    fn send(self) -> Result<(), crate::operations::OperationError> {
+        info!("🎥 Sending video...");
+
+        let url = format!(
+            "{root_url}{token}/sendVideo",
+            root_url = API_ROOT_URL,
+            token = self.params.1.token
+        );
+        trace!("url: {}", url);
+
+        let req_instance: SendVideoRequestModel = self.params.into();
+        let req_body = match req_instance.try_into() {
+            Ok(f) => f,
+            Err(e) => return Err(e),
+        };
+        debug!("request body: {:?}", req_body);
+
+        let client = Client::new();
+        let response = client
+            .post(url)
+            .multipart(req_body)
+            .send()
+            .map_err(|err| OperationError::ReqwestError(err))?;
+        let data = response
+            .json::<GenericResponseModel>()
+            .map_err(|err| OperationError::ReqwestError(err))?;
+
+        if let Some(description) = data.description {
+            return Err(OperationError::TelegramAPIError { description });
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/operations/mod.rs b/src/operations/mod.rs
new file mode 100644
index 0000000..97e1fbe
--- /dev/null
+++ b/src/operations/mod.rs
@@ -0,0 +1,72 @@
+use std::{error, fmt};
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+pub(crate) mod bot;
+
+#[derive(Debug)]
+pub(crate) struct RootParams {
+    #[allow(dead_code)]
+    verbosity: u8,
+}
+
+impl RootParams {
+    pub(crate) fn new(verbosity: u8) -> Self {
+        Self { verbosity }
+    }
+}
+
+#[derive(Debug)]
+pub enum OperationError {
+    IoError(std::io::Error),
+    ParseFloatError(std::num::ParseFloatError),
+    LoggerError(log::SetLoggerError),
+    ReqwestError(reqwest::Error),
+    SerdeJsonError(serde_json::Error),
+    MissingArgument { subc_name: String, arg_name: String },
+    TelegramAPIError { description: String },
+}
+
+impl error::Error for OperationError {}
+
+impl fmt::Display for OperationError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            OperationError::IoError(err) => err.fmt(f),
+            OperationError::ParseFloatError(err) => err.fmt(f),
+            OperationError::LoggerError(err) => err.fmt(f),
+            OperationError::ReqwestError(err) => err.fmt(f),
+            OperationError::MissingArgument {
+                subc_name,
+                arg_name,
+            } => write!(
+                f,
+                "`{}` is a required argument for `{}` subcommand but is missing.",
+                arg_name, subc_name
+            ),
+            OperationError::TelegramAPIError { description } => {
+                write!(f, "An error occured on Telegram API: {}", description)
+            }
+            OperationError::SerdeJsonError(err) => err.fmt(f),
+        }
+    }
+}
+
+impl OperationError {
+    pub fn exit_code(&self) -> i32 {
+        // should we cover exit code for each error kind?
+        match self {
+            OperationError::IoError(_) => 100,
+            OperationError::ParseFloatError(_) => 200,
+            OperationError::LoggerError(_) => 300,
+            OperationError::ReqwestError(_) => 400,
+            OperationError::MissingArgument { .. } => 500,
+            OperationError::TelegramAPIError { .. } => 600,
+            OperationError::SerdeJsonError(_) => 700,
+        }
+    }
+}
diff --git a/tgcli/__init__.py b/tgcli/__init__.py
deleted file mode 100644
index 5829031..0000000
--- a/tgcli/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-__author__ = "Eray Erdin"
-__version__ = "0.4.0pre3"
diff --git a/tgcli/cli/__init__.py b/tgcli/cli/__init__.py
deleted file mode 100644
index b71782f..0000000
--- a/tgcli/cli/__init__.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import platform
-
-import click
-
-import tgcli.request.bot
-from tgcli.cli.bot import bot
-
-IS_DARWIN = platform.system().lower() == "darwin"
-
-MEDIA_TYPES = [str(v.value) for v in list(tgcli.request.bot.MediaType)]
-
-
-########
-# Root #
-########
-@click.group()
-@click.option(
-    "--secure/--no-secure",
-    default=(not IS_DARWIN),
-    help='Whether to validate HTTPS requests. Default is "secure" except OSX. '
-    'You have to update built-in OpenSSL and manually pass "secure" each '
-    "time in OSX.",
-)
-@click.pass_context
-def cli(ctx, secure: bool):
-    ctx.obj = dict()
-    ctx.obj["secure"] = secure
-
-
-cli.add_command(bot)
diff --git a/tgcli/cli/bot/__init__.py b/tgcli/cli/bot/__init__.py
deleted file mode 100644
index af640e2..0000000
--- a/tgcli/cli/bot/__init__.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import click
-
-from tgcli.cli.bot.send import send
-
-
-@click.group()
-@click.option(
-    "-t",
-    "--token",
-    envvar="TELEGRAM_BOT_TOKEN",
-    required=True,
-    help="""Token of bot. Can be provided via TELEGRAM_BOT_TOKEN environment
-    variable.""",
-)
-@click.pass_context
-def bot(ctx, token):
-    ctx.obj["token"] = token
-
-
-bot.add_command(send)
diff --git a/tgcli/cli/bot/send.py b/tgcli/cli/bot/send.py
deleted file mode 100644
index 6156045..0000000
--- a/tgcli/cli/bot/send.py
+++ /dev/null
@@ -1,234 +0,0 @@
-import io
-import sys
-import typing
-
-import click
-import colorful
-import yaspin
-import yaspin.spinners
-
-import tgcli.request.bot
-
-MESSAGE_FORMATS = {"html": "HTML", "markdown": "Markdown"}
-
-
-def send_message(
-    session: tgcli.request.bot.BotSession, request: tgcli.request.bot.BotRequest
-):
-    """
-    Sends message using Yaspin.
-    """
-    with yaspin.yaspin(yaspin.spinners.Spinners.clock) as spinner:
-        spinner.text = "Sending message..."
-        response = session.send(request)
-
-        if response.ok:
-            spinner.text = "Message was sent successfully."
-            spinner.ok("✔️ ")
-        else:
-            data = response.json()
-
-            code = data.get("error_code", "Unknown")
-            description = data.get("description", "No description found.")
-            spinner.write(
-                "{c.bold_red}Error Code:{c.reset} {}".format(code, c=colorful)
-            )
-            spinner.write(
-                "{c.bold_red}Error Details:{c.reset} {}".format(description, c=colorful)
-            )
-
-            spinner.text = "Failed sending message."
-            spinner.fail("❌")
-            sys.exit(1)
-
-
-@click.group()
-@click.option("-r", "--receiver", required=True, help="Receiver of the message.")
-@click.option(
-    "--format",
-    "format_",
-    default="markdown",
-    type=click.Choice(MESSAGE_FORMATS.keys()),
-    help='Format of the message. Default is "markdown".',
-)
-@click.pass_context
-def send(ctx, receiver: str, format_: str):
-    ctx.obj["receiver"] = receiver
-    ctx.obj["format"] = format_
-
-
-@send.command()
-@click.argument("message", required=True)
-@click.pass_context
-def message(ctx, message: str):
-    session = tgcli.request.bot.BotSession(ctx.obj["token"])
-    session.verify = ctx.obj["secure"]
-    receiver = ctx.obj["receiver"]
-    format = ctx.obj["format"]
-
-    request = tgcli.request.bot.SendMessageRequest(
-        session, receiver, message, MESSAGE_FORMATS[format]
-    )
-
-    send_message(session, request)
-
-
-@send.command()
-@click.option(
-    "-o",
-    "--option",
-    "options",
-    multiple=True,
-    help="An option for poll question. You can define multiple option.",
-)
-@click.argument("question", required=True)
-@click.pass_context
-def poll(ctx, options: typing.Tuple[str], question: str):
-    if len(options) < 2:
-        error_messages = (
-            "You need to provide at least two options.",
-            "You have provided only one option."
-            if len(options) == 1
-            else "You have provided no options.",
-        )
-
-        raise click.BadParameter(" ".join(error_messages))
-
-    session = tgcli.request.bot.BotSession(ctx.obj["token"])
-    session.verify = ctx.obj["secure"]
-    receiver = ctx.obj["receiver"]
-
-    request = tgcli.request.bot.SendPollRequest(session, receiver, question, options)
-
-    send_message(session, request)
-
-
-@send.command()
-@click.option(
-    "-x", "--latitude", type=click.FLOAT, required=True, help="Latitude of location.",
-)
-@click.option(
-    "-y", "--longitude", type=click.FLOAT, required=True, help="Longitude of location.",
-)
-@click.pass_context
-def location(ctx, latitude: float, longitude: float):
-    session = tgcli.request.bot.BotSession(ctx.obj["token"])
-    session.verify = ctx.obj["secure"]
-    receiver = ctx.obj["receiver"]
-
-    request = tgcli.request.bot.SendLocationRequest(
-        session, receiver, latitude, longitude
-    )
-
-    send_message(session, request)
-
-
-THUMBNAIL_OPTION = click.option("--thumbnail", type=click.File("rb"))
-FILE_ARGUMENT = click.argument("file", type=click.File("rb"), required=True)
-
-
-@send.command()
-@click.option("-m", "--message", default="", help="The message to inline with file.")
-@THUMBNAIL_OPTION
-@FILE_ARGUMENT
-@click.pass_context
-def document(ctx, message: str, thumbnail: io.BytesIO, file: io.BytesIO):
-    session = tgcli.request.bot.BotSession(ctx.obj["token"])
-    session.verify = ctx.obj["secure"]
-    receiver = ctx.obj["receiver"]
-    format = ctx.obj["format"]
-
-    request = tgcli.request.bot.SendDocumentRequest(
-        session, receiver, file, thumbnail, message, MESSAGE_FORMATS[format]
-    )
-    file.close()
-
-    send_message(session, request)
-
-
-@send.command()
-@click.option("-m", "--message", default="", help="The message to inline with file.")
-@FILE_ARGUMENT
-@click.pass_context
-def photo(ctx, message: str, file: io.BytesIO):
-    session = tgcli.request.bot.BotSession(ctx.obj["token"])
-    session.verify = ctx.obj["secure"]
-    receiver = ctx.obj["receiver"]
-    format = ctx.obj["format"]
-
-    request = tgcli.request.bot.SendPhotoRequest(
-        session, receiver, file, message, MESSAGE_FORMATS[format]
-    )
-    file.close()
-
-    send_message(session, request)
-
-
-@send.command()
-@click.option("-m", "--message", default="", help="The message to inline with file.")
-@click.option(
-    "-w",
-    "--width",
-    type=click.INT,
-    default=1920,
-    help="The width of the video. It does not affect the video itself, "
-    "just affects the shape of video container in the application. Defaults to 1920.",
-)
-@click.option(
-    "-h",
-    "--height",
-    type=click.INT,
-    default=1080,
-    help="The height of the video. It does not affect the video itself, "
-    "just affects the shape of video container in the application. Defaults to 1080.",
-)
-@FILE_ARGUMENT
-@click.pass_context
-def video(ctx, message: str, width: int, height: int, file: io.BytesIO):
-    session = tgcli.request.bot.BotSession(ctx.obj["token"])
-    session.verify = ctx.obj["secure"]
-    receiver = ctx.obj["receiver"]
-    format = ctx.obj["format"]
-
-    request = tgcli.request.bot.SendVideoRequest(
-        session,
-        receiver,
-        file,
-        None,
-        message,
-        None,
-        width,
-        height,
-        MESSAGE_FORMATS[format],
-    )
-    file.close()
-
-    send_message(session, request)
-
-
-@send.command()
-@click.option("-m", "--message", default="", help="The message to inline with file.")
-@click.option("--performer", help="The performer of audio.")
-@click.option("--title", help="The title of audio.")
-@FILE_ARGUMENT
-@click.pass_context
-def audio(ctx, message: str, performer: str, title: str, file: io.BytesIO):
-    session = tgcli.request.bot.BotSession(ctx.obj["token"])
-    session.verify = ctx.obj["secure"]
-    receiver = ctx.obj["receiver"]
-    format = ctx.obj["format"]
-
-    request = tgcli.request.bot.SendAudioRequest(
-        session,
-        receiver,
-        file,
-        None,
-        message,
-        None,
-        performer,
-        title,
-        MESSAGE_FORMATS[format],
-    )
-    file.close()
-
-    send_message(session, request)
diff --git a/tgcli/request/__init__.py b/tgcli/request/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tgcli/request/bot.py b/tgcli/request/bot.py
deleted file mode 100644
index dd2face..0000000
--- a/tgcli/request/bot.py
+++ /dev/null
@@ -1,285 +0,0 @@
-import enum
-import io
-import typing
-
-import requests
-
-
-class BotSession(requests.Session):
-    def __init__(self, token: str):
-        super().__init__()
-        self.token = str(token)
-        self._is_mocked = False
-
-    @property
-    def base_url(self):
-        if self._is_mocked:
-            scheme = "mock"
-        else:
-            scheme = "https"
-
-        return "{scheme}://api.telegram.org/bot{token}/".format(
-            scheme=scheme, token=self.token
-        )
-
-
-class BotRequest(requests.PreparedRequest):
-    def __init__(self, session: BotSession, method_name: str):
-        super().__init__()
-        self.__session = session
-        self.__method_name = method_name
-        self.prepare()
-
-    def prepare_url(self, url, params):
-        super(BotRequest, self).prepare_url(
-            "{base_url}{method_name}".format(
-                base_url=self.__session.base_url, method_name=self.__method_name,
-            ),
-            params,
-        )
-
-
-class AuthenticationRequest(BotRequest):
-    def __init__(self, session: BotSession):
-        super().__init__(session, "getMe")
-        self.prepare_method("post")
-
-
-class SendMessageRequest(BotRequest):
-    def __init__(
-        self,
-        session: BotSession,
-        chat_id: typing.Union[str, int],
-        text: str,
-        parse_mode: str = "Markdown",
-        disable_web_page_preview: bool = False,
-        disable_notification: bool = False,
-    ):
-        try:
-            chat_id = int(chat_id)
-        except ValueError:  # pragma: no cover
-            pass  # pragma: no cover
-
-        super().__init__(session, "sendMessage")
-        self.prepare_method("post")
-        payload = {
-            "chat_id": chat_id,
-            "text": str(text),
-            "parse_mode": str(parse_mode),
-            "disable_web_page_preview": bool(disable_web_page_preview),
-            "disable_notification": bool(disable_notification),
-        }
-        self.prepare_body(data=None, files=None, json=payload)
-
-
-class SendPollRequest(BotRequest):
-    def __init__(
-        self,
-        session: BotSession,
-        chat_id: typing.Union[str, int],
-        question: str,
-        options: typing.Iterable[str],
-        disable_notification: bool = False,
-    ):
-        try:
-            chat_id = int(chat_id)
-        except ValueError:  # pragma: no cover
-            pass  # pragma: no cover
-
-        super().__init__(session, "sendPoll")
-        self.prepare_method("post")
-        payload = {
-            "chat_id": chat_id,
-            "question": str(question),
-            "options": tuple(options),
-            "disable_notification": bool(disable_notification),
-        }
-        self.prepare_body(data=None, files=None, json=payload)
-
-
-class SendLocationRequest(BotRequest):
-    def __init__(
-        self,
-        session: BotSession,
-        chat_id: typing.Union[str, int],
-        latitude: float,
-        longitude: float,
-        disable_notification: bool = False,
-    ):
-        try:
-            chat_id = int(chat_id)
-        except ValueError:  # pragma: no cover
-            pass  # pragma: no cover
-
-        super().__init__(session, "sendLocation")
-        self.prepare_method("post")
-        payload = {
-            "chat_id": chat_id,
-            "latitude": float(latitude),
-            "longitude": float(longitude),
-            "disable_notification": bool(disable_notification),
-        }
-        self.prepare_body(data=None, files=None, json=payload)
-
-
-@enum.unique
-class MediaType(enum.Enum):
-    DOCUMENT = {"method_name": "sendDocument", "parameter_name": "document"}
-    PHOTO = {"method_name": "sendPhoto", "parameter_name": "photo"}
-    VIDEO = {"method_name": "sendVideo", "parameter_name": "video"}
-    AUDIO = {"method_name": "sendAudio", "parameter_name": "audio"}
-
-
-class BaseFileRequest(BotRequest):
-    def __init__(
-        self,
-        session: BotSession,
-        chat_id: typing.Union[str, int],
-        file: io.BytesIO,
-        caption: str,
-        media_type: MediaType,
-        parse_mode: str = "Markdown",
-        disable_notification: bool = False,
-        **kwargs
-    ):
-        try:
-            chat_id = int(chat_id)
-        except ValueError:  # pragma: no cover
-            pass  # pragma: no cover
-
-        super().__init__(session, media_type.value.get("method_name"))
-        extra_files = kwargs.pop("extra_files", dict())
-
-        payload = {
-            "chat_id": chat_id,
-            "caption": str(caption),
-            "parse_mode": str(parse_mode),
-            "disable_notification": bool(disable_notification),
-        }
-        payload = dict(**payload, **kwargs)
-        self.prepare_method("post")
-        self.prepare_body(
-            data=payload,
-            files={str(media_type.value.get("parameter_name")): file, **extra_files,},
-            json=None,
-        )
-        self.file = file
-
-
-class SendDocumentRequest(BaseFileRequest):
-    def __init__(
-        self,
-        session: BotSession,
-        chat_id: typing.Union[str, int],
-        document: io.BytesIO,
-        thumbnail: io.BytesIO = None,
-        caption: str = "",
-        parse_mode: str = "Markdown",
-        disable_notification: bool = False,
-    ):
-        extra_files = {"thumb": thumbnail}
-        payload = {
-            "session": session,
-            "chat_id": chat_id,
-            "file": document,
-            "caption": caption,
-            "parse_mode": parse_mode,
-            "disable_notification": disable_notification,
-            "media_type": MediaType.DOCUMENT,
-        }
-        if thumbnail:
-            super().__init__(**payload, **extra_files)
-        else:
-            super().__init__(**payload)
-
-
-class SendPhotoRequest(BaseFileRequest):
-    def __init__(
-        self,
-        session: BotSession,
-        chat_id: typing.Union[str, int],
-        photo: io.BytesIO,
-        caption: str = "",
-        parse_mode: str = "Markdown",
-        disable_notification: bool = False,
-    ):
-        payload = {
-            "session": session,
-            "chat_id": chat_id,
-            "file": photo,
-            "caption": caption,
-            "parse_mode": parse_mode,
-            "disable_notification": disable_notification,
-            "media_type": MediaType.PHOTO,
-        }
-        super().__init__(**payload)
-
-
-class SendAudioRequest(BaseFileRequest):
-    def __init__(
-        self,
-        session: BotSession,
-        chat_id: typing.Union[str, int],
-        audio: io.BytesIO,
-        thumbnail: io.BytesIO = None,
-        caption: str = "",
-        duration: int = None,
-        performer: str = None,
-        title: str = None,
-        parse_mode: str = "Markdown",
-        disable_notification: bool = False,
-    ):
-        extra_files = {"thumb": thumbnail}
-        payload = {
-            "session": session,
-            "chat_id": chat_id,
-            "file": audio,
-            "caption": caption,
-            "parse_mode": parse_mode,
-            "disable_notification": disable_notification,
-            "media_type": MediaType.AUDIO,
-            **(
-                {"duration": int(duration)} if duration is not None else {}
-            ),  # ref https://stackoverflow.com/a/14263905/2926992
-            **({"performer": str(performer)} if performer is not None else {}),
-            **({"title": str(title)} if title is not None else {}),
-        }
-
-        if thumbnail:
-            super().__init__(**payload, **extra_files)
-        else:
-            super().__init__(**payload)
-
-
-class SendVideoRequest(BaseFileRequest):
-    def __init__(
-        self,
-        session: BotSession,
-        chat_id: typing.Union[str, int],
-        video: io.BytesIO,
-        thumbnail: io.BytesIO = None,
-        caption: str = "",
-        duration: int = None,
-        width: int = None,
-        height: int = None,
-        parse_mode: str = "Markdown",
-        disable_notification: bool = False,
-    ):
-        extra_files = {"thumb": thumbnail}
-        payload = {
-            "session": session,
-            "chat_id": chat_id,
-            "file": video,
-            "caption": caption,
-            "parse_mode": parse_mode,
-            "disable_notification": disable_notification,
-            "media_type": MediaType.VIDEO,
-            **({"duration": int(duration)} if duration is not None else {}),
-            **({"width": int(width)} if width is not None else {}),
-            **({"height": int(height)} if height is not None else {}),
-        }
-
-        if thumbnail:
-            super().__init__(**payload, **extra_files)
-        else:
-            super().__init__(**payload)


Test patch:
diff --git a/.github/workflows/test.linux.yml b/.github/workflows/test.linux.yml
new file mode 100644
index 0000000..cfa92e2
--- /dev/null
+++ b/.github/workflows/test.linux.yml
@@ -0,0 +1,35 @@
+name: test_linux
+
+on: [push, pull_request]
+
+jobs:
+  Test:
+    strategy:
+      matrix:
+        os: [ubuntu-latest]
+        rust: [stable, beta]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - uses: actions/checkout@v1
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: ${{ matrix.rust }}
+          override: true
+      - uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cargo/registry
+            ~/.cargo/git
+            target
+          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+      - uses: actions-rs/cargo@v1
+        env:
+          TELEGRAM_BOT_TOKEN: ${{ secrets.SECRET_TELEGRAM_BOT_TOKEN }}
+          TELEGRAM_RECEIVER: ${{ secrets.SECRET_TELEGRAM_RECEIVER }}
+          TELEGRAM_RECEIVER_ABSENT: ${{ secrets.SECRET_TELEGRAM_RECEIVER_ABSENT }}
+        with:
+          command: test
+          args: --workspace
diff --git a/.github/workflows/test.macos.yml b/.github/workflows/test.macos.yml
new file mode 100644
index 0000000..e634ddb
--- /dev/null
+++ b/.github/workflows/test.macos.yml
@@ -0,0 +1,35 @@
+name: test_macos
+
+on: [push, pull_request]
+
+jobs:
+  Test:
+    strategy:
+      matrix:
+        os: [macos-latest]
+        rust: [stable]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - uses: actions/checkout@v1
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: ${{ matrix.rust }}
+          override: true
+      - uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cargo/registry
+            ~/.cargo/git
+            target
+          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+      - uses: actions-rs/cargo@v1
+        env:
+          TELEGRAM_BOT_TOKEN: ${{ secrets.SECRET_TELEGRAM_BOT_TOKEN }}
+          TELEGRAM_RECEIVER: ${{ secrets.SECRET_TELEGRAM_RECEIVER }}
+          TELEGRAM_RECEIVER_ABSENT: ${{ secrets.SECRET_TELEGRAM_RECEIVER_ABSENT }}
+        with:
+          command: test
+          args: --workspace -- --nocapture
diff --git a/.github/workflows/test.windows.yml b/.github/workflows/test.windows.yml
new file mode 100644
index 0000000..a57d419
--- /dev/null
+++ b/.github/workflows/test.windows.yml
@@ -0,0 +1,35 @@
+name: test_windows
+
+on: [push, pull_request]
+
+jobs:
+  Test:
+    strategy:
+      matrix:
+        os: [windows-latest]
+        rust: [stable]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - uses: actions/checkout@v1
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: ${{ matrix.rust }}
+          override: true
+      - uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cargo/registry
+            ~/.cargo/git
+            target
+          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+      - uses: actions-rs/cargo@v1
+        env:
+          TELEGRAM_BOT_TOKEN: ${{ secrets.SECRET_TELEGRAM_BOT_TOKEN }}
+          TELEGRAM_RECEIVER: ${{ secrets.SECRET_TELEGRAM_RECEIVER }}
+          TELEGRAM_RECEIVER_ABSENT: ${{ secrets.SECRET_TELEGRAM_RECEIVER_ABSENT }}
+        with:
+          command: test
+          args: --workspace -- --nocapture
diff --git a/conftest.py b/conftest.py
deleted file mode 100644
index 4c0ae63..0000000
--- a/conftest.py
+++ /dev/null
@@ -1,1 +0,0 @@
-pytest_plugins = ("tests.fixtures",)
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index 661f047..0000000
--- a/pytest.ini
+++ /dev/null
@@ -1,7 +0,0 @@
-[pytest]
-python_files =
-    test_*.py
-addopts = -s
-console_output_style = progress
-log_cli_level = DEBUG
-log_print = True
diff --git a/tests/resources/placeholder.wav b/resources/test/audio.wav
similarity index 100%
rename from tests/resources/placeholder.wav
rename to resources/test/audio.wav
diff --git a/resources/test/doc.txt b/resources/test/doc.txt
new file mode 100644
index 0000000..b04cc96
--- /dev/null
+++ b/resources/test/doc.txt
@@ -0,0 +1,1 @@
+This is a test document to send.
diff --git a/resources/test/thumbnail512.png b/resources/test/thumbnail512.png
new file mode 100644
index 0000000..c6217c0
Binary files /dev/null and b/resources/test/thumbnail512.png differ
diff --git a/tests/resources/placeholder.mkv b/resources/test/video.mkv
similarity index 100%
rename from tests/resources/placeholder.mkv
rename to resources/test/video.mkv
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/bot_send_audio.rs b/tests/bot_send_audio.rs
new file mode 100644
index 0000000..8b259a3
--- /dev/null
+++ b/tests/bot_send_audio.rs
@@ -0,0 +1,54 @@
+use std::env;
+
+use assert_cmd::Command;
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[macro_use]
+extern crate rstest;
+
+#[fixture]
+fn binary() -> Command {
+    let _ = dotenv::dotenv();
+    Command::cargo_bin("tgcli").expect("Could not find tgcli binary.")
+}
+
+#[rstest]
+fn send_audio(mut binary: Command) {
+    let assertion = binary
+        .args([
+            "bot",
+            "send",
+            "--receiver",
+            &env::var("TELEGRAM_RECEIVER")
+                .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+            "audio",
+            "resources/test/audio.wav",
+        ])
+        .assert();
+
+    assertion.success();
+}
+
+#[rstest]
+fn send_audio_with_metadata(mut binary: Command) {
+    let assertion = binary
+        .args([
+            "bot",
+            "send",
+            "--receiver",
+            &env::var("TELEGRAM_RECEIVER")
+                .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+            "audio",
+            "resources/test/audio.wav",
+            "--performer", "Eray Erdin",
+            "--title", "White Noise",
+        ])
+        .assert();
+
+    assertion.success();
+}
diff --git a/tests/bot_send_document.rs b/tests/bot_send_document.rs
new file mode 100644
index 0000000..a57bdcf
--- /dev/null
+++ b/tests/bot_send_document.rs
@@ -0,0 +1,94 @@
+use std::env;
+
+use assert_cmd::Command;
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[macro_use]
+extern crate rstest;
+
+#[fixture]
+fn binary() -> Command {
+    let _ = dotenv::dotenv();
+    Command::cargo_bin("tgcli").expect("Could not find tgcli binary.")
+}
+
+#[rstest]
+fn send_document(mut binary: Command) {
+    let assertion = binary
+        .args([
+            "bot",
+            "send",
+            "--receiver",
+            &env::var("TELEGRAM_RECEIVER")
+                .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+            "document",
+            "resources/test/doc.txt",
+        ])
+        .assert();
+
+    assertion.success();
+}
+
+#[rstest]
+fn send_document_with_message(mut binary: Command) {
+    let assertion = binary
+        .args([
+            "bot",
+            "send",
+            "--receiver",
+            &env::var("TELEGRAM_RECEIVER")
+                .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+            "document",
+            "resources/test/doc.txt",
+            "--message",
+            "example document",
+        ])
+        .assert();
+
+    assertion.success();
+}
+
+#[rstest]
+fn send_document_with_long_message(mut binary: Command) {
+    let msg = (0..1034).map(|_| "a").collect::<String>();
+
+    let assertion = binary
+        .args([
+            "bot",
+            "send",
+            "--receiver",
+            &env::var("TELEGRAM_RECEIVER")
+                .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+            "document",
+            "resources/test/doc.txt",
+            "--message",
+            &msg,
+        ])
+        .assert();
+
+    assertion.failure();
+}
+
+#[rstest]
+fn send_document_with_thumbnail(mut binary: Command) {
+    let assertion = binary
+        .args([
+            "bot",
+            "send",
+            "--receiver",
+            &env::var("TELEGRAM_RECEIVER")
+                .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+            "document",
+            "resources/test/doc.txt",
+            "--thumbnail",
+            "resources/test/thumbnail512.png",
+        ])
+        .assert();
+
+    assertion.success();
+}
diff --git a/tests/bot_send_location.rs b/tests/bot_send_location.rs
new file mode 100644
index 0000000..662b932
--- /dev/null
+++ b/tests/bot_send_location.rs
@@ -0,0 +1,50 @@
+use std::env;
+
+use assert_cmd::Command;
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[macro_use]
+extern crate rstest;
+
+#[fixture]
+fn binary() -> Command {
+    let _ = dotenv::dotenv();
+    Command::cargo_bin("tgcli").expect("Could not find tgcli binary.")
+}
+
+#[rstest]
+fn send_location(mut binary: Command) {
+    let assertion = binary.args([
+        "bot",
+        "send",
+        "--receiver",
+        &env::var("TELEGRAM_RECEIVER")
+            .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+        "location",
+        "-x", "38.42",
+        "-y", "27.14",
+    ]).assert();
+
+    assertion.success();
+}
+
+#[rstest]
+fn send_location_invalid_float(mut binary: Command, #[values("a", "0..1", "1..0")] latitude: &str) {
+    let assertion = binary.args([
+        "bot",
+        "send",
+        "--receiver",
+        &env::var("TELEGRAM_RECEIVER")
+            .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+        "location",
+        "-x", latitude,
+        "-y", "27.14",
+    ]).assert();
+
+    assertion.failure();
+}
diff --git a/tests/bot_send_message.rs b/tests/bot_send_message.rs
new file mode 100644
index 0000000..2155f9c
--- /dev/null
+++ b/tests/bot_send_message.rs
@@ -0,0 +1,111 @@
+use std::env;
+
+use assert_cmd::Command;
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[macro_use]
+extern crate rstest;
+
+#[fixture]
+fn binary() -> Command {
+    let _ = dotenv::dotenv();
+    Command::cargo_bin("tgcli").expect("Could not find tgcli binary.")
+}
+
+#[rstest]
+fn send_message_to_present_receiver(mut binary: Command) {
+    let assertion = binary
+        .args([
+            "bot",
+            "send",
+            "--receiver",
+            &env::var("TELEGRAM_RECEIVER")
+                .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+            "message",
+            "foo",
+        ])
+        .assert();
+
+    assertion.success();
+}
+
+#[rstest]
+fn send_message_to_absent_receiver(mut binary: Command) {
+    let assertion = binary
+        .args([
+            "bot",
+            "send",
+            "--receiver",
+            &env::var("TELEGRAM_RECEIVER_ABSENT")
+                .expect("TELEGRAM_RECEIVER_ABSENT environment variable could not be found. Please create .env file and define it."),
+            "message",
+            "foo",
+        ])
+        .assert();
+
+    assertion.failure();
+}
+
+#[rstest]
+fn send_message_format(mut binary: Command, #[values("markdown", "html")] format: &str) {
+    let assertion = binary
+        .args([
+            "bot",
+            "send",
+            "--receiver",
+            &env::var("TELEGRAM_RECEIVER")
+                .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+            "--format",
+            format,
+            "message",
+            if format == "html" {
+                r#"<strong>foo</strong>"#
+            } else {
+                "*foo*"
+            },
+        ])
+        .assert();
+
+    assertion.success();
+}
+
+#[rstest]
+fn send_message_silent(mut binary: Command) {
+    let assertion = binary
+        .args([
+            "bot",
+            "send",
+            "--receiver",
+            &env::var("TELEGRAM_RECEIVER")
+                .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+            "--silent",
+            "message",
+            "foo",
+        ])
+        .assert();
+
+    assertion.success();
+}
+
+#[rstest]
+fn send_message_protect_content(mut binary: Command) {
+    let assertion = binary
+        .args([
+            "bot",
+            "send",
+            "--receiver",
+            &env::var("TELEGRAM_RECEIVER")
+                .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+            "--protect-content",
+            "message",
+            "protected message",
+        ])
+        .assert();
+
+    assertion.success();
+}
diff --git a/tests/bot_send_photo.rs b/tests/bot_send_photo.rs
new file mode 100644
index 0000000..a5fa536
--- /dev/null
+++ b/tests/bot_send_photo.rs
@@ -0,0 +1,54 @@
+use std::env;
+
+use assert_cmd::Command;
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[macro_use]
+extern crate rstest;
+
+#[fixture]
+fn binary() -> Command {
+    let _ = dotenv::dotenv();
+    Command::cargo_bin("tgcli").expect("Could not find tgcli binary.")
+}
+
+#[rstest]
+fn send_photo(mut binary: Command) {
+    let assertion = binary
+        .args([
+            "bot",
+            "send",
+            "--receiver",
+            &env::var("TELEGRAM_RECEIVER")
+                .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+            "photo",
+            "resources/test/thumbnail512.png",
+        ])
+        .assert();
+
+    assertion.success();
+}
+
+#[rstest]
+fn send_photo_with_message(mut binary: Command) {
+    let assertion = binary
+        .args([
+            "bot",
+            "send",
+            "--receiver",
+            &env::var("TELEGRAM_RECEIVER")
+                .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+            "photo",
+            "resources/test/thumbnail512.png",
+            "--message",
+            "example photo",
+        ])
+        .assert();
+
+    assertion.success();
+}
diff --git a/tests/bot_send_poll.rs b/tests/bot_send_poll.rs
new file mode 100644
index 0000000..111f984
--- /dev/null
+++ b/tests/bot_send_poll.rs
@@ -0,0 +1,88 @@
+use std::env;
+
+use assert_cmd::Command;
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[macro_use]
+extern crate rstest;
+
+#[fixture]
+fn binary() -> Command {
+    let _ = dotenv::dotenv();
+    Command::cargo_bin("tgcli").expect("Could not find tgcli binary.")
+}
+
+#[rstest]
+fn send_poll(mut binary: Command) {
+    let assertion = binary.args([
+        "bot",
+        "send",
+        "--receiver",
+        &env::var("TELEGRAM_RECEIVER")
+            .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+        "poll",
+        "foo",
+        "-o", "bar",
+        "-o", "baz",
+    ]).assert();
+
+    assertion.success();
+}
+
+#[rstest]
+fn send_poll_insufficient_options(mut binary: Command) {
+    let assertion = binary.args([
+        "bot",
+        "send",
+        "--receiver",
+        &env::var("TELEGRAM_RECEIVER")
+            .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+        "poll",
+        "foo",
+        "-o", "bar",
+    ]).assert();
+
+    assertion.failure();
+}
+
+#[rstest]
+fn send_poll_overused_chars_question(mut binary: Command) {
+    let question = (0..301).map(|_| "a").collect::<String>();
+
+    let assertion = binary.args([
+        "bot",
+        "send",
+        "--receiver",
+        &env::var("TELEGRAM_RECEIVER")
+            .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+        "poll",
+        &question,
+        "-o", "bar",
+    ]).assert();
+
+    assertion.failure();
+}
+
+#[rstest]
+fn send_poll_overused_chars_option(mut binary: Command) {
+    let option = (0..101).map(|_| "a").collect::<String>();
+
+    let assertion = binary.args([
+        "bot",
+        "send",
+        "--receiver",
+        &env::var("TELEGRAM_RECEIVER")
+            .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+        "poll",
+        "foo",
+        "-o", &option,
+        "-o", "bar",
+    ]).assert();
+
+    assertion.failure();
+}
diff --git a/tests/bot_send_video.rs b/tests/bot_send_video.rs
new file mode 100644
index 0000000..d385f09
--- /dev/null
+++ b/tests/bot_send_video.rs
@@ -0,0 +1,35 @@
+use std::env;
+
+use assert_cmd::Command;
+
+// Copyright (c) 2022 Eray Erdin
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#[macro_use]
+extern crate rstest;
+
+#[fixture]
+fn binary() -> Command {
+    let _ = dotenv::dotenv();
+    Command::cargo_bin("tgcli").expect("Could not find tgcli binary.")
+}
+
+#[rstest]
+fn send_video(mut binary: Command) {
+    let assertion = binary
+        .args([
+            "bot",
+            "send",
+            "--receiver",
+            &env::var("TELEGRAM_RECEIVER")
+                .expect("TELEGRAM_RECEIVER environment variable could not be found. Please create .env file and define it."),
+            "video",
+            "resources/test/video.mkv",
+        ])
+        .assert();
+
+    assertion.success();
+}
diff --git a/tests/fixtures.py b/tests/fixtures.py
deleted file mode 100644
index 74abbd8..0000000
--- a/tests/fixtures.py
+++ /dev/null
@@ -1,128 +0,0 @@
-import io
-import os
-import typing
-
-import pytest
-import requests_mock
-
-import tgcli.request.bot
-from tgcli.cli import cli as cli_root
-
-
-@pytest.fixture
-def mock_adapter() -> requests_mock.Adapter:
-    """
-    Returns a mock adapter.
-    """
-    return requests_mock.Adapter()
-
-
-@pytest.fixture
-def cli():
-    return cli_root
-
-
-@pytest.fixture
-def bot_session(mock_adapter) -> tgcli.request.bot.BotSession:
-    """
-    Creates a bot session. It is mounted with a mock adapter.
-
-    Fixtures
-    --------
-    mock_adapter
-
-    Attributes
-    ----------
-    token = "0"
-    _is_mocked = True
-    """
-    session = tgcli.request.bot.BotSession("0")
-    session.mount("mock", mock_adapter)
-    session._is_mocked = True
-    return session
-
-
-@pytest.fixture
-def bot_request_factory(bot_session) -> callable:
-    """
-    Returns a factory that creates a bot request.
-
-    Fixtures
-    --------
-    bot_session
-    """
-
-    def factory(method_name: str) -> tgcli.request.bot.BotRequest:
-        return tgcli.request.bot.BotRequest(bot_session, method_name)
-
-    return factory
-
-
-@pytest.fixture
-def bot_authentication_request(bot_session) -> tgcli.request.bot.AuthenticationRequest:
-    """
-    Returns a bot authentication request.
-
-    Fixtures
-    --------
-    bot_session
-
-    Attributes
-    ----------
-    session = bot_session
-    """
-    return tgcli.request.bot.AuthenticationRequest(bot_session)
-
-
-@pytest.fixture
-def location() -> typing.Tuple[float]:
-    """
-    Returns an example latitude and longitude.
-    """
-    return (38.4219611, 27.0941414)
-
-
-@pytest.fixture
-def file_factory(request):
-    """
-    Returns a factory which opens a file in rb mode and properly closes it.
-
-    Fixtures
-    --------
-    request - In order to use finalizer.
-    """
-
-    def factory(file_path: str) -> io.FileIO:
-        file = open(file_path, "rb")
-        request.addfinalizer(lambda: file.close())
-        return file
-
-    return factory
-
-
-@pytest.fixture
-def bot_token() -> typing.Union[str, None]:
-    """
-    Returns TELEGRAM_BOT_TOKEN environment variable.
-    """
-    return os.environ.get("TELEGRAM_BOT_TOKEN")
-
-
-@pytest.fixture
-def receiver_id() -> typing.Union[str, None]:
-    """
-    Returns TELEGRAM_RECEIVER environment variable.
-    """
-    return os.environ.get("TELEGRAM_RECEIVER")
-
-
-@pytest.fixture
-def invoke_message_factory():
-    """
-    A factory to generate message for Telegram.
-    """
-
-    def factory(klass: typing.ClassVar, method: typing.Callable):
-        return "A message invoked by `{}::{}`.".format(klass.__name__, method.__name__)
-
-    return factory
diff --git a/tests/resources/file.png b/tests/resources/file.png
deleted file mode 100644
index 48dc299..0000000
Binary files a/tests/resources/file.png and /dev/null differ
diff --git a/tests/resources/responses/bot.TestAuthenticationRequest.1.json b/tests/resources/responses/bot.TestAuthenticationRequest.1.json
deleted file mode 100644
index 5a37d68..0000000
--- a/tests/resources/responses/bot.TestAuthenticationRequest.1.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-    "ok": true,
-    "result":{
-        "id": 1,
-        "is_bot": true,
-        "first_name": "FooBot",
-        "username": "foobot"
-    }
-}
diff --git a/tests/resources/responses/bot.TestAuthenticationRequest.2.json b/tests/resources/responses/bot.TestAuthenticationRequest.2.json
deleted file mode 100644
index f5e4f01..0000000
--- a/tests/resources/responses/bot.TestAuthenticationRequest.2.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-    "ok": false,
-    "error_code": 404,
-    "description": "Not Found"
-}
diff --git a/tests/test_cli/__init__.py b/tests/test_cli/__init__.py
deleted file mode 100644
index 39ae0a2..0000000
--- a/tests/test_cli/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import os
-
-import pytest
-import requests
-
-try:
-    requests.get("https://telegram.org")
-    _HAS_CONNECTED = True
-except Exception:
-    _HAS_CONNECTED = False
-
-
-_BOT_TOKEN_EXISTS = True
-if os.environ.get("TELEGRAM_BOT_TOKEN") is None:
-    _BOT_TOKEN_EXISTS = False
-
-
-_BOT_RECEIVER_EXISTS = True
-if os.environ.get("TELEGRAM_RECEIVER") is None:
-    _BOT_RECEIVER_EXISTS = False
-
-
-SKIPIF_HAS_NOT_CONNECTED = pytest.mark.skipif(
-    not _HAS_CONNECTED,
-    reason="This test requires a stable connection to telegram.org.",
-)
-SKIPIF_BOT_TOKEN_NOT_EXISTS = pytest.mark.skipif(
-    not _BOT_TOKEN_EXISTS,
-    reason="This test requires TELEGRAM_BOT_TOKEN environment variable to be set.",
-)
-SKIPIF_BOT_RECEIVER_NOT_EXISTS = pytest.mark.skipif(
-    not _BOT_RECEIVER_EXISTS,
-    reason="This test requires TELEGRAM_RECEIVER environment variable to be set.",
-)
diff --git a/tests/test_cli/test_bot_send.py b/tests/test_cli/test_bot_send.py
deleted file mode 100644
index ae7c409..0000000
--- a/tests/test_cli/test_bot_send.py
+++ /dev/null
@@ -1,308 +0,0 @@
-from click.testing import CliRunner
-
-from tests.test_cli import (
-    SKIPIF_BOT_RECEIVER_NOT_EXISTS,
-    SKIPIF_BOT_TOKEN_NOT_EXISTS,
-    SKIPIF_HAS_NOT_CONNECTED,
-)
-
-
-class TestMessage:
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_html(
-        self, cli_runner: CliRunner, cli, receiver_id: str, invoke_message_factory,
-    ):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "--format",
-            "html",
-            "message",
-            invoke_message_factory(self.__class__, self.test_html),
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
-
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_markdown(
-        self, cli_runner: CliRunner, cli, receiver_id: str, invoke_message_factory,
-    ):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "--format",
-            "markdown",
-            "message",
-            invoke_message_factory(self.__class__, self.test_markdown),
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
-
-
-class TestDocument:
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_vanilla(self, cli_runner: CliRunner, cli, receiver_id: str):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "document",
-            "tests/resources/file.png",
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
-
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_thumbnail(self, cli_runner: CliRunner, cli, receiver_id: str):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "document",
-            "setup.py",
-            "--thumbnail",
-            "tests/resources/file.png",
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
-        # todo does not send thumbnail
-
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_message(
-        self, cli_runner: CliRunner, cli, receiver_id: str, invoke_message_factory,
-    ):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "document",
-            "setup.py",
-            "-m",
-            invoke_message_factory(self.__class__, self.test_message),
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
-
-
-class TestPhoto:
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_vanilla(self, cli_runner: CliRunner, cli, receiver_id: str):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "photo",
-            "tests/resources/file.png",
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
-
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_message(
-        self, cli_runner: CliRunner, cli, receiver_id: str, invoke_message_factory,
-    ):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "photo",
-            "tests/resources/file.png",
-            "-m",
-            invoke_message_factory(self.__class__, self.test_vanilla),
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
-
-
-class TestVideo:
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_vanilla(self, cli_runner: CliRunner, cli, receiver_id: str):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "video",
-            "tests/resources/placeholder.mkv",
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
-
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_aspect_ratio(self, cli_runner: CliRunner, cli, receiver_id: str):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "video",
-            "tests/resources/placeholder.mkv",
-            "-w",
-            "16",
-            "-h",
-            "9",
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
-
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_message(
-        self, cli_runner: CliRunner, cli, receiver_id: str, invoke_message_factory,
-    ):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "video",
-            "tests/resources/placeholder.mkv",
-            "-m",
-            invoke_message_factory(self.__class__, self.test_message),
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
-
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_message_aspect_ratio(
-        self, cli_runner: CliRunner, cli, receiver_id: str, invoke_message_factory,
-    ):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "video",
-            "tests/resources/placeholder.mkv",
-            "-m",
-            invoke_message_factory(self.__class__, self.test_message_aspect_ratio),
-            "-w",
-            "16",
-            "-h",
-            "9",
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
-
-
-class TestAudio:
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_vanilla(self, cli_runner: CliRunner, cli, receiver_id: str):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "audio",
-            "tests/resources/placeholder.wav",
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
-
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_performer(self, cli_runner: CliRunner, cli, receiver_id: str):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "audio",
-            "tests/resources/placeholder.wav",
-            "--performer",
-            "Eray Erdin",
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
-
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_title(self, cli_runner: CliRunner, cli, receiver_id: str):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "audio",
-            "tests/resources/placeholder.wav",
-            "--title",
-            "White Noise",
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
-
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_message(
-        self, cli_runner: CliRunner, cli, receiver_id: str, invoke_message_factory
-    ):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "audio",
-            "tests/resources/placeholder.wav",
-            "-m",
-            invoke_message_factory(self.__class__, self.test_message),
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
-
-
-# todo polls cannot be tested because it cannot be sent to private messages
-
-
-class TestLocation:
-    @SKIPIF_HAS_NOT_CONNECTED
-    @SKIPIF_BOT_TOKEN_NOT_EXISTS
-    @SKIPIF_BOT_RECEIVER_NOT_EXISTS
-    def test_vanilla(self, cli_runner: CliRunner, cli, receiver_id: str):
-        args = (
-            "bot",
-            "send",
-            "-r",
-            receiver_id,
-            "location",
-            "-x",
-            "38.41273",
-            "-y",
-            "27.13838",
-        )
-        result = cli_runner.invoke(cli, args)
-        assert result.exit_code == 0
diff --git a/tests/test_request/__init__.py b/tests/test_request/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/test_request/test_bot.py b/tests/test_request/test_bot.py
deleted file mode 100644
index 92ffc1c..0000000
--- a/tests/test_request/test_bot.py
+++ /dev/null
@@ -1,113 +0,0 @@
-import tgcli.request.bot
-
-
-class TestBotSession:
-    def test_has_token(self, bot_session):
-        assert hasattr(bot_session, "token")
-
-    def test_has_base_url(self, bot_session):
-        assert hasattr(bot_session, "base_url")
-
-
-class TestBotRequest:
-    def setup_method(self):
-        self.session = tgcli.request.bot.BotSession("0")
-        self.request = tgcli.request.bot.BotRequest(self.session, "getMe")
-
-    def test_url(self, bot_session, bot_request_factory):
-        bot_request = bot_request_factory("getMe")
-        url = "{base_url}{method_name}".format(
-            base_url=bot_session.base_url, method_name="getMe"
-        )
-        assert bot_request.url == url
-
-
-class TestAuthenticationRequest:
-    def test_url(self, bot_authentication_request):
-        assert bot_authentication_request.url[-5:] == "getMe"
-
-    def test_found_status(self, mock_adapter, bot_session, bot_authentication_request):
-        with open(
-            "tests/resources/responses/bot.TestAuthenticationRequest.1.json"
-        ) as f:
-            mock_adapter.register_uri(
-                "POST", bot_authentication_request.url, text=f.read(), status_code=200,
-            )
-
-        response = bot_session.send(bot_authentication_request)
-        assert response.status_code == 200
-
-    def test_found_ok(self, mock_adapter, bot_session, bot_authentication_request):
-        with open(
-            "tests/resources/responses/bot.TestAuthenticationRequest.1.json"
-        ) as f:
-            mock_adapter.register_uri(
-                "POST", bot_authentication_request.url, text=f.read(), status_code=200,
-            )
-
-        response = bot_session.send(bot_authentication_request)
-        assert response.json().get("ok")
-
-    def test_found_result(self, mock_adapter, bot_session, bot_authentication_request):
-        with open(
-            "tests/resources/responses/bot.TestAuthenticationRequest.1.json"
-        ) as f:
-            mock_adapter.register_uri(
-                "POST", bot_authentication_request.url, text=f.read(), status_code=200,
-            )
-
-        response = bot_session.send(bot_authentication_request)
-        assert response.json().get("result").get("id") == 1
-        assert response.json().get("result").get("is_bot")
-        assert response.json().get("result").get("first_name") == "FooBot"
-        assert response.json().get("result").get("username") == "foobot"
-
-    def test_notfound_status(
-        self, mock_adapter, bot_session, bot_authentication_request
-    ):
-        with open(
-            "tests/resources/responses/bot.TestAuthenticationRequest.2.json"
-        ) as f:
-            mock_adapter.register_uri(
-                "POST", bot_authentication_request.url, text=f.read(), status_code=404,
-            )
-
-        response = bot_session.send(bot_authentication_request)
-        assert response.status_code == 404
-
-    def test_notfound_ok(self, mock_adapter, bot_session, bot_authentication_request):
-        with open(
-            "tests/resources/responses/bot.TestAuthenticationRequest.2.json"
-        ) as f:
-            mock_adapter.register_uri(
-                "POST", bot_authentication_request.url, text=f.read(), status_code=404,
-            )
-
-        response = bot_session.send(bot_authentication_request)
-        assert not response.json().get("ok")
-
-    def test_notfound_error_code(
-        self, mock_adapter, bot_session, bot_authentication_request
-    ):
-        with open(
-            "tests/resources/responses/bot.TestAuthenticationRequest.2.json"
-        ) as f:
-            mock_adapter.register_uri(
-                "POST", bot_authentication_request.url, text=f.read(), status_code=404,
-            )
-
-        response = bot_session.send(bot_authentication_request)
-        assert response.json().get("error_code") == 404
-
-    def test_notfound_description(
-        self, mock_adapter, bot_session, bot_authentication_request
-    ):
-        with open(
-            "tests/resources/responses/bot.TestAuthenticationRequest.2.json"
-        ) as f:
-            mock_adapter.register_uri(
-                "POST", bot_authentication_request.url, text=f.read(), status_code=404,
-            )
-
-        response = bot_session.send(bot_authentication_request)
-        assert response.json().get("description") == "Not Found"