diff --git a/.dockerignore b/.dockerignore
index 94a969dc1f2264778bbdf7ecd0917c3cc4c7f9be..4154867b1d308d6f4c92e46df27b00d7fe985168 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,3 +1,3 @@
 /target
 .env
-Dockerfile
\ No newline at end of file
+Dockerfile
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index af965808505f24eca6498781a71d48d5c6268168..d9f37096bf3bdd440eba3f41da533a78fa1982e5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -7,6 +7,8 @@
 # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
 stages:
   - test
+  - build
+  - deploy
 
 .test-template:
   tags:
@@ -39,14 +41,50 @@ fmt:
     - pre-commit run fmt -av
 
 build_img:
+  stage: build
   tags:
     - kube
   only:
-    - master
+    refs:
+      - master
+    changes:
+      - "src/**/*"
+      - "Cargo.toml"
+      - "Cargo.lock"
   image:
     name: r.j3ss.co/img
     entrypoint: [""]
+
   script:
     - img login --password "${DOCKER_PASSWORD}" --username "${DOCKER_USER}" "${DOCKER_REGISTRY}"
     - img build --no-console -t docker.le-memese.com/bots/s3bot:latest .
     - img push docker.le-memese.com/bots/s3bot:latest
+
+deploy:
+  stage: deploy
+  tags:
+    - kube
+  only:
+    refs:
+      - master
+  needs:
+    - build_img
+  image:
+    name: alpine/helm:3.7.1
+    entrypoint: ["/bin/sh", "-c"]
+  environment:
+    name: prod
+    action: start
+    url: "https://s3bot.le-memese.com"
+  script:
+    - helm
+      upgrade
+      s3bot
+      ./deploy/helm
+      --install
+      --wait
+      --create-namespace
+      --atomic
+      --timeout 2m
+      --namespace "$NAMESPACE"
+      -f "$HELM_CONFIG"
diff --git a/Cargo.lock b/Cargo.lock
index 48e9db9dfe516c23feb6dfd77fa6d6a5c598e352..f7f525ab9722b92b4f44ef29ba7f969579131646 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -72,7 +72,7 @@ dependencies = [
  "mime",
  "percent-encoding",
  "pin-project-lite",
- "rand",
+ "rand 0.8.5",
  "sha1 0.10.5",
  "smallvec",
  "tokio",
@@ -230,7 +230,7 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.8",
  "once_cell",
  "version_check",
 ]
@@ -333,6 +333,19 @@ dependencies = [
  "toml",
 ]
 
+[[package]]
+name = "async-compression"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a"
+dependencies = [
+ "flate2",
+ "futures-core",
+ "memchr",
+ "pin-project-lite",
+ "tokio",
+]
+
 [[package]]
 name = "async-trait"
 version = "0.1.64"
@@ -554,6 +567,16 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "core-foundation-sys"
 version = "0.8.3"
@@ -774,6 +797,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
 [[package]]
 name = "fern"
 version = "0.6.1"
@@ -801,6 +833,21 @@ 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.1.0"
@@ -810,6 +857,16 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "futf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
 [[package]]
 name = "futures"
 version = "0.3.26"
@@ -909,6 +966,17 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.8"
@@ -931,7 +999,7 @@ dependencies = [
  "num-bigint",
  "num-integer",
  "num-traits",
- "rand",
+ "rand 0.8.5",
 ]
 
 [[package]]
@@ -947,6 +1015,7 @@ dependencies = [
  "grammers-mtsender",
  "grammers-session",
  "grammers-tl-types",
+ "html5ever",
  "locate-locale",
  "log",
  "md5",
@@ -964,7 +1033,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "573508524fd529ced63fe827fb89629a186414541fd9e645fef9ba499fc69c63"
 dependencies = [
  "aes",
- "getrandom",
+ "getrandom 0.2.8",
  "glass_pumpkin",
  "hmac",
  "num-bigint",
@@ -982,7 +1051,7 @@ dependencies = [
  "bytes",
  "crc32fast",
  "flate2",
- "getrandom",
+ "getrandom 0.2.8",
  "grammers-crypto",
  "grammers-tl-types",
  "log",
@@ -1108,6 +1177,20 @@ dependencies = [
  "digest 0.9.0",
 ]
 
+[[package]]
+name = "html5ever"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "http"
 version = "0.2.8"
@@ -1119,6 +1202,17 @@ dependencies = [
  "itoa",
 ]
 
+[[package]]
+name = "http-body"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "http-range"
 version = "0.1.5"
@@ -1143,6 +1237,43 @@ version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
 
+[[package]]
+name = "hyper"
+version = "0.14.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c"
+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 = "iana-time-zone"
 version = "0.1.53"
@@ -1187,6 +1318,15 @@ dependencies = [
  "hashbrown",
 ]
 
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
 [[package]]
 name = "io-lifetimes"
 version = "1.0.5"
@@ -1197,6 +1337,12 @@ dependencies = [
  "windows-sys 0.45.0",
 ]
 
+[[package]]
+name = "ipnet"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
+
 [[package]]
 name = "is-terminal"
 version = "0.4.3"
@@ -1322,6 +1468,26 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "markup5ever"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
+dependencies = [
+ "log",
+ "phf",
+ "phf_codegen",
+ "string_cache",
+ "string_cache_codegen",
+ "tendril",
+]
+
 [[package]]
 name = "md5"
 version = "0.7.0"
@@ -1386,6 +1552,30 @@ dependencies = [
  "windows-sys 0.42.0",
 ]
 
+[[package]]
+name = "native-tls"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
+
 [[package]]
 name = "nom"
 version = "7.1.3"
@@ -1405,7 +1595,7 @@ dependencies = [
  "autocfg",
  "num-integer",
  "num-traits",
- "rand",
+ "rand 0.8.5",
 ]
 
 [[package]]
@@ -1449,6 +1639,51 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
+[[package]]
+name = "openssl"
+version = "0.10.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "os_info"
 version = "3.6.0"
@@ -1495,7 +1730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "77e0b28ace46c5a396546bcf443bf422b57049617433d8854227352a4a9b24e7"
 dependencies = [
  "base64ct",
- "rand_core",
+ "rand_core 0.6.4",
  "subtle",
 ]
 
@@ -1524,6 +1759,63 @@ version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
 
+[[package]]
+name = "phf"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
+dependencies = [
+ "phf_shared 0.8.0",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
+dependencies = [
+ "phf_generator 0.8.0",
+ "phf_shared 0.8.0",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
+dependencies = [
+ "phf_shared 0.8.0",
+ "rand 0.7.3",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
+dependencies = [
+ "phf_shared 0.10.0",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher",
+]
+
 [[package]]
 name = "pin-project-lite"
 version = "0.2.9"
@@ -1548,6 +1840,12 @@ version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
 
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
 [[package]]
 name = "proc-macro-error"
 version = "1.0.4"
@@ -1601,6 +1899,20 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc",
+ "rand_pcg",
+]
+
 [[package]]
 name = "rand"
 version = "0.8.5"
@@ -1608,8 +1920,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "libc",
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
 ]
 
 [[package]]
@@ -1619,7 +1941,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 dependencies = [
  "ppv-lite86",
- "rand_core",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
 ]
 
 [[package]]
@@ -1628,7 +1959,25 @@ version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.8",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
+dependencies = [
+ "rand_core 0.5.1",
 ]
 
 [[package]]
@@ -1679,6 +2028,70 @@ version = "0.6.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
 
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.11.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9"
+dependencies = [
+ "async-compression",
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-rustls",
+ "tokio-util",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "rustc_version"
 version = "0.4.0"
@@ -1702,6 +2115,18 @@ dependencies = [
  "windows-sys 0.45.0",
 ]
 
+[[package]]
+name = "rustls"
+version = "0.20.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
+dependencies = [
+ "log",
+ "ring",
+ "sct",
+ "webpki",
+]
+
 [[package]]
 name = "ryu"
 version = "1.0.12"
@@ -1726,14 +2151,26 @@ dependencies = [
  "futures",
  "grammers-client",
  "grammers-session",
+ "lazy_static",
  "log",
- "rand",
+ "rand 0.8.5",
  "rayon",
+ "regex",
+ "reqwest",
  "serde",
  "serde_json",
  "tokio",
 ]
 
+[[package]]
+name = "schannel"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
+dependencies = [
+ "windows-sys 0.42.0",
+]
+
 [[package]]
 name = "scopeguard"
 version = "1.1.0"
@@ -1746,6 +2183,39 @@ version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2"
 
+[[package]]
+name = "sct"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "semver"
 version = "1.0.16"
@@ -1843,6 +2313,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "siphasher"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+
 [[package]]
 name = "slab"
 version = "0.4.7"
@@ -1868,6 +2344,38 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "string_cache"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08"
+dependencies = [
+ "new_debug_unreachable",
+ "once_cell",
+ "parking_lot",
+ "phf_shared 0.10.0",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro2",
+ "quote",
+]
+
 [[package]]
 name = "strsim"
 version = "0.10.0"
@@ -1891,6 +2399,31 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
 [[package]]
 name = "termcolor"
 version = "1.2.0"
@@ -1984,6 +2517,27 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.23.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
+dependencies = [
+ "rustls",
+ "tokio",
+ "webpki",
+]
+
 [[package]]
 name = "tokio-util"
 version = "0.7.7"
@@ -2007,6 +2561,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
 [[package]]
 name = "tracing"
 version = "0.1.37"
@@ -2028,6 +2588,12 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "try-lock"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
+
 [[package]]
 name = "typenum"
 version = "1.16.0"
@@ -2070,6 +2636,12 @@ version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
 
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
 [[package]]
 name = "url"
 version = "2.3.1"
@@ -2081,12 +2653,40 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[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.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
+[[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.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
 [[package]]
 name = "wasi"
 version = "0.10.0+wasi-snapshot-preview1"
@@ -2124,6 +2724,18 @@ dependencies = [
  "wasm-bindgen-shared",
 ]
 
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
 [[package]]
 name = "wasm-bindgen-macro"
 version = "0.2.84"
@@ -2153,6 +2765,26 @@ version = "0.2.84"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
 
+[[package]]
+name = "web-sys"
+version = "0.3.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
 [[package]]
 name = "winapi"
 version = "0.2.8"
@@ -2277,6 +2909,15 @@ version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
 
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "zstd"
 version = "0.12.3+zstd.1.5.2"
diff --git a/Cargo.toml b/Cargo.toml
index bc66a3b666993989f4e18730e03b3bdafae004c2..fbc8a78bd4d69a0e6911da6956149abfa7c15f45 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,11 +18,14 @@ dotenvy = "^0.15.6"
 dyn-clone = "1.0.10"
 fern = { version = "0.6.1", features = ["chrono", "colored"] }
 futures = "0.3.26"
-grammers-client = { version = "0.4.0", features = ["markdown"] }
+grammers-client = { version = "0.4.0", features = ["markdown", "html"] }
 grammers-session = "0.4.0"
+lazy_static = "1.4.0"
 log = "0.4.17"
 rand = "0.8.5"
 rayon = "1.6.1"
+regex = "1.7.1"
+reqwest = { version = "0.11.14", features = ["gzip", "json", "tokio-rustls"] }
 serde = { version = "1.0.152", features = ["derive"] }
 serde_json = "1.0.93"
 tokio = { version = "1.25.0", features = [
diff --git a/Dockerfile b/Dockerfile
index 54495390c433c5ac6f2239133c8490b2bdf62a54..9c43cb2713d106694696514412405e12b83da785 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,6 +4,7 @@ WORKDIR /app
 COPY Cargo.toml Cargo.lock askama.toml ./
 COPY src ./src
 COPY static ./static
+# Build binary in release mode.
 RUN cargo build --release --all-features
 
 FROM debian:bullseye-20230109-slim as base
@@ -16,12 +17,16 @@ RUN apt-get update \
 
 COPY static ./static
 
+# Copy built binary to a new image.
 COPY --from=builder /app/target/release/s3bot /usr/local/bin/
 
 ENTRYPOINT ["/usr/local/bin/s3bot"]
 
 FROM base as rootless
 
+# Create a user and make the image rootless. So no one
+# can escalate privileges even if they have access to
+# container.
 RUN useradd --create-home  -u 1000 --user-group s3bot
 WORKDIR /home/s3bot
 RUN mv /static ./static
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..37a940ead71f2401b9f1c4f1c32704a039386cbc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,38 @@
+<div align="center">
+  <img src="./logo.png" width="300">
+  <h1 align="center">
+    Automated telegram account
+  </h1>
+</div>
+
+This project is an attempt to add some utillities to your account.
+
+This app uses telegram API and connects to it as a user, not as a bot. To use it, you need to register a new application, and obtain `api hash token` and `application id` from official telegram website. You can do it [here](https://core.telegram.org/api/obtaining_api_id).
+
+
+## How to
+
+First of all, you need to install [Rust](http://rust-lang.org/). Personally I recommend to use [rustup](https://rustup.rs/).
+
+Make sure everything is fine by running `cargo --version`.
+
+1. Compile the app.
+    ```bash
+    cargo build --release
+    ```
+    After that command you'll find a compiled binary in `target/release/s3bot`.
+
+2. Run the compiled binary.
+
+
+## Configuration
+
+You can configure this app by either command line arguments or with environment variables.
+
+If you place .env file in current working directory, contents will be loaded as environment variables.
+
+For additional help please use:
+
+```bash
+s3bot --help
+```
\ No newline at end of file
diff --git a/helm/.helmignore b/helm/.helmignore
new file mode 100644
index 0000000000000000000000000000000000000000..0e8a0eb36f4ca2c939201c0d54b5d82a1ea34778
--- /dev/null
+++ b/helm/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/helm/Chart.yaml b/helm/Chart.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..0978bd78de08dbbfe43c3683263a7cb8e30cbcb2
--- /dev/null
+++ b/helm/Chart.yaml
@@ -0,0 +1,24 @@
+apiVersion: v2
+name: s3bot
+description: A Helm chart for Kubernetes
+
+# A chart can be either an 'application' or a 'library' chart.
+#
+# Application charts are a collection of templates that can be packaged into versioned archives
+# to be deployed.
+#
+# Library charts provide useful utilities or functions for the chart developer. They're included as
+# a dependency of application charts to inject those utilities and functions into the rendering
+# pipeline. Library charts do not define any templates and therefore cannot be deployed.
+type: application
+
+# This is the chart version. This version number should be incremented each time you make changes
+# to the chart and its templates, including the app version.
+# Versions are expected to follow Semantic Versioning (https://semver.org/)
+version: 0.1.0
+
+# This is the version number of the application being deployed. This version number should be
+# incremented each time you make changes to the application. Versions are not expected to
+# follow Semantic Versioning. They should reflect the version the application is using.
+# It is recommended to use it with quotes.
+appVersion: "latest"
diff --git a/helm/templates/NOTES.txt b/helm/templates/NOTES.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d39da692a8f1ecede816a7fe1fa85f08c1e6a604
--- /dev/null
+++ b/helm/templates/NOTES.txt
@@ -0,0 +1,22 @@
+1. Get the application URL by running these commands:
+{{- if .Values.ingress.enabled }}
+{{- range $host := .Values.ingress.hosts }}
+  {{- range .paths }}
+  http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
+  {{- end }}
+{{- end }}
+{{- else if contains "NodePort" .Values.service.type }}
+  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "s3bot.fullname" . }})
+  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
+  echo http://$NODE_IP:$NODE_PORT
+{{- else if contains "LoadBalancer" .Values.service.type }}
+     NOTE: It may take a few minutes for the LoadBalancer IP to be available.
+           You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "s3bot.fullname" . }}'
+  export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "s3bot.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
+  echo http://$SERVICE_IP:{{ .Values.service.port }}
+{{- else if contains "ClusterIP" .Values.service.type }}
+  export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "s3bot.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
+  export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
+  echo "Visit http://127.0.0.1:8080 to use your application"
+  kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
+{{- end }}
diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..129e6f954ed0ac4dd634b8c8842e8c1de16dd00c
--- /dev/null
+++ b/helm/templates/_helpers.tpl
@@ -0,0 +1,62 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "s3bot.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "s3bot.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "s3bot.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "s3bot.labels" -}}
+helm.sh/chart: {{ include "s3bot.chart" . }}
+{{ include "s3bot.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "s3bot.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "s3bot.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "s3bot.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "s3bot.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d6d2c0be0592dd1d12d5f95ab1d31dea5b6cd13a
--- /dev/null
+++ b/helm/templates/deployment.yaml
@@ -0,0 +1,68 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "s3bot.fullname" . }}
+  labels:
+    {{- include "s3bot.labels" . | nindent 4 }}
+spec:
+  {{- if not .Values.autoscaling.enabled }}
+  replicas: 1
+  {{- end }}
+  selector:
+    matchLabels:
+      {{- include "s3bot.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      {{- with .Values.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        {{- include "s3bot.selectorLabels" . | nindent 8 }}
+    spec:
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      serviceAccountName: {{ include "s3bot.serviceAccountName" . }}
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
+      containers:
+        - name: {{ .Chart.Name }}
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          ports:
+            - name: http
+              containerPort: {{ default 8000 .Values.env.BOT_SERVER_PORT }}
+              protocol: TCP
+          livenessProbe:
+            httpGet:
+              path: /health
+              port: http
+          readinessProbe:
+            httpGet:
+              path: /health
+              port: http
+          resources:
+            {{- toYaml .Values.resources | nindent 12 }}
+          {{- with .Values.env }}
+          env:
+            {{- range $key, $val := . }}
+            - name: {{ $key | quote }}
+              value: {{ $val | quote }}
+            {{- end }}
+          {{- end }}
+      {{- with .Values.nodeSelector }}
+      nodeSelector:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
diff --git a/helm/templates/ingress.yaml b/helm/templates/ingress.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b37b206cfe11e62a2004f57b59a2b25c5d0bd404
--- /dev/null
+++ b/helm/templates/ingress.yaml
@@ -0,0 +1,61 @@
+{{- if .Values.ingress.enabled -}}
+{{- $fullName := include "s3bot.fullname" . -}}
+{{- $svcPort := .Values.service.port -}}
+{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
+  {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
+  {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
+  {{- end }}
+{{- end }}
+{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
+apiVersion: networking.k8s.io/v1
+{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
+apiVersion: networking.k8s.io/v1beta1
+{{- else -}}
+apiVersion: extensions/v1beta1
+{{- end }}
+kind: Ingress
+metadata:
+  name: {{ $fullName }}
+  labels:
+    {{- include "s3bot.labels" . | nindent 4 }}
+  {{- with .Values.ingress.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+spec:
+  {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
+  ingressClassName: {{ .Values.ingress.className }}
+  {{- end }}
+  {{- if .Values.ingress.tls }}
+  tls:
+    {{- range .Values.ingress.tls }}
+    - hosts:
+        {{- range .hosts }}
+        - {{ . | quote }}
+        {{- end }}
+      secretName: {{ .secretName }}
+    {{- end }}
+  {{- end }}
+  rules:
+    {{- range .Values.ingress.hosts }}
+    - host: {{ .host | quote }}
+      http:
+        paths:
+          {{- range .paths }}
+          - path: {{ .path }}
+            {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
+            pathType: {{ .pathType }}
+            {{- end }}
+            backend:
+              {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
+              service:
+                name: {{ $fullName }}
+                port:
+                  number: {{ $svcPort }}
+              {{- else }}
+              serviceName: {{ $fullName }}
+              servicePort: {{ $svcPort }}
+              {{- end }}
+          {{- end }}
+    {{- end }}
+{{- end }}
diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..06b58d561c5e7e54476994fd03b327f3080356ed
--- /dev/null
+++ b/helm/templates/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "s3bot.fullname" . }}
+  labels:
+    {{- include "s3bot.labels" . | nindent 4 }}
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - port: {{ .Values.service.port }}
+      targetPort: http
+      protocol: TCP
+      name: http
+  selector:
+    {{- include "s3bot.selectorLabels" . | nindent 4 }}
diff --git a/helm/templates/serviceaccount.yaml b/helm/templates/serviceaccount.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..0b01689f598cf4646ee507042254a3405fd3a12a
--- /dev/null
+++ b/helm/templates/serviceaccount.yaml
@@ -0,0 +1,12 @@
+{{- if .Values.serviceAccount.create -}}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: {{ include "s3bot.serviceAccountName" . }}
+  labels:
+    {{- include "s3bot.labels" . | nindent 4 }}
+  {{- with .Values.serviceAccount.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+{{- end }}
diff --git a/helm/templates/tests/test-connection.yaml b/helm/templates/tests/test-connection.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..092ba623b424162c33de0a17aaa363ac99b0951a
--- /dev/null
+++ b/helm/templates/tests/test-connection.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Pod
+metadata:
+  name: "{{ include "s3bot.fullname" . }}-test-connection"
+  labels:
+    {{- include "s3bot.labels" . | nindent 4 }}
+  annotations:
+    "helm.sh/hook": test
+spec:
+  containers:
+    - name: wget
+      image: busybox
+      command: ['wget']
+      args: ['{{ include "s3bot.fullname" . }}:{{ .Values.service.port }}']
+  restartPolicy: Never
diff --git a/helm/values.yaml b/helm/values.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b6e61d22321a1f48b44d3ae79faa27f23f4c31cb
--- /dev/null
+++ b/helm/values.yaml
@@ -0,0 +1,89 @@
+# Default values for s3bot.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+image:
+  repository: docker.le-memese.com/bots/s3bot
+  pullPolicy: Always
+  # Overrides the image tag whose default is the chart appVersion.
+  tag: ""
+
+env:
+  BOT_SERVER_HOST: "0.0.0.0"
+  BOT_SERVER_PORT: 8000
+  BOT_SERVER_USERNAME: "s3rius_san"
+
+imagePullSecrets: []
+nameOverride: ""
+fullnameOverride: ""
+
+serviceAccount:
+  # Specifies whether a service account should be created
+  create: true
+  # Annotations to add to the service account
+  annotations: {}
+  # The name of the service account to use.
+  # If not set and create is true, a name is generated using the fullname template
+  name: ""
+
+podAnnotations: {}
+
+podSecurityContext:
+  {}
+  # fsGroup: 2000
+
+securityContext:
+  {}
+  # capabilities:
+  #   drop:
+  #   - ALL
+  # readOnlyRootFilesystem: true
+  # runAsNonRoot: true
+  # runAsUser: 1000
+
+service:
+  type: ClusterIP
+  port: 80
+
+ingress:
+  enabled: false
+  className: ""
+  annotations:
+    {}
+    # kubernetes.io/ingress.class: nginx
+    # kubernetes.io/tls-acme: "true"
+  hosts:
+    - host: chart-example.local
+      paths:
+        - path: /
+          pathType: ImplementationSpecific
+  tls: []
+  #  - secretName: chart-example-tls
+  #    hosts:
+  #      - chart-example.local
+
+resources:
+  {}
+  # We usually recommend not to specify default resources and to leave this as a conscious
+  # choice for the user. This also increases chances charts run on environments with little
+  # resources, such as Minikube. If you do want to specify resources, uncomment the following
+  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
+  # limits:
+  #   cpu: 100m
+  #   memory: 128Mi
+  # requests:
+  #   cpu: 100m
+  #   memory: 128Mi
+
+autoscaling:
+  enabled: false
+  minReplicas: 1
+  maxReplicas: 100
+  targetCPUUtilizationPercentage: 80
+  # targetMemoryUtilizationPercentage: 80
+
+nodeSelector: {}
+
+tolerations: []
+
+affinity: {}
diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..f0c18852f0ceba3af1f519e355cb9a9dfac8be13
Binary files /dev/null and b/logo.png differ
diff --git a/src/args.rs b/src/args.rs
index ca9fc82077ff83c15ef7f05f45f29d2416d4c7b9..15ab61b19805f7e82c996a9940abba6681e6419e 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -77,8 +77,21 @@ pub struct BotConfig {
     )]
     pub session_file: String,
 
-    #[arg(name = "bot-excluded-chats", long, env = "BOT_EXCLUDED_CHATS")]
+    #[arg(
+        name = "bot-excluded-chats",
+        long,
+        env = "BOT_EXCLUDED_CHATS",
+        value_delimiter = ','
+    )]
     pub excluded_chats: Vec<i64>,
+
+    #[arg(
+        name = "bot-currency-excluded-chats",
+        long,
+        env = "BOT_CURRENCY_EXCLUDED_CHATS",
+        value_delimiter = ','
+    )]
+    pub currency_excluded_chats: Vec<i64>,
 }
 
 #[derive(Clone, Parser, Debug)]
diff --git a/src/bot/filters/chain.rs b/src/bot/filters/filtered_handler.rs
similarity index 76%
rename from src/bot/filters/chain.rs
rename to src/bot/filters/filtered_handler.rs
index 14f57e7b858a9ad456a0e17155c239a886fbee11..44fd29f9d7426246dfce2b3862b6eb89565dde7d 100644
--- a/src/bot/filters/chain.rs
+++ b/src/bot/filters/filtered_handler.rs
@@ -4,6 +4,11 @@ use crate::bot::handlers::Handler;
 
 use super::base::Filter;
 
+/// This is a structure to match
+/// handlers with corresponding filters.
+///
+/// It's handy, because for different messages
+/// we have different set of rules.
 #[derive(Clone)]
 pub struct FilteredHandler {
     filters: Vec<Box<dyn Filter>>,
@@ -23,6 +28,8 @@ impl FilteredHandler {
         self
     }
 
+    /// This method performs checks for all filters we have.
+    /// We run it not in parralel for fast fail strategy.
     pub fn check(&self, update: &Update) -> bool {
         for filter in &self.filters {
             match filter.filter(update) {
diff --git a/src/bot/filters/groups.rs b/src/bot/filters/groups.rs
deleted file mode 100644
index 8b137891791fe96927ad78e64b0aad7bded08bdc..0000000000000000000000000000000000000000
--- a/src/bot/filters/groups.rs
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/bot/filters/message_fitlers.rs b/src/bot/filters/message_fitlers.rs
index 0c8ea458f2ad0a553cf15b3404219613c351bf17..f2d973d20848cffd4328f954b429c54b674f109f 100644
--- a/src/bot/filters/message_fitlers.rs
+++ b/src/bot/filters/message_fitlers.rs
@@ -1,5 +1,7 @@
 use grammers_client::Update;
 
+use crate::utils::messages::get_message;
+
 use super::base::Filter;
 
 #[allow(dead_code)]
@@ -15,9 +17,17 @@ pub enum TextMatchMethod {
     IMatches,
 }
 
+/// This filter is used to filter out
+/// that marked as silent.
+#[derive(Clone)]
+pub struct SilentFilter;
+
+/// This filter checks that current
+/// chat is not one of excluded chats.
 #[derive(Clone)]
 pub struct ExcludedChatsFilter(pub Vec<i64>);
 
+/// This filter checks for message directions.
 #[derive(Clone)]
 pub struct MessageDirectionFilter(pub MessageDirection);
 
@@ -31,10 +41,8 @@ impl Filter for ExcludedChatsFilter {
             Update::CallbackQuery(query) => query.chat().clone(),
             _ => return Ok(false),
         };
-        if self.0.contains(&a.id()) {
-            return Ok(false);
-        }
-        Ok(true)
+        // Check that list of excluded chats doesn't contain our chat.
+        Ok(!self.0.contains(&a.id()))
     }
 }
 
@@ -44,6 +52,7 @@ impl Filter for MessageDirectionFilter {
 
         let res = matches!(
             (self.0, message.outgoing()),
+            // Here we check that message's direction matches the direction we want.
             (MessageDirection::Outgoing, true) | (MessageDirection::Incoming, false)
         );
         Ok(res)
@@ -72,3 +81,11 @@ impl<'a> Filter for TextFilter<'a> {
         Ok(false)
     }
 }
+
+impl Filter for SilentFilter {
+    fn filter(&self, update: &Update) -> anyhow::Result<bool> {
+        let Some(message) = get_message(update) else {return Ok(false)};
+        // Check that message is not silent.
+        Ok(!message.silent())
+    }
+}
diff --git a/src/bot/filters/mod.rs b/src/bot/filters/mod.rs
index 51fe228303aff09c868cf87148887a45cfe05f07..9b169f65c8ca930922c8675f1073758a0743f8c0 100644
--- a/src/bot/filters/mod.rs
+++ b/src/bot/filters/mod.rs
@@ -1,3 +1,3 @@
-mod base;
-pub mod chain;
+pub mod base;
+pub mod filtered_handler;
 pub mod message_fitlers;
diff --git a/src/bot/handlers/basic/currency_converter.rs b/src/bot/handlers/basic/currency_converter.rs
new file mode 100644
index 0000000000000000000000000000000000000000..57f854ce08a747c3f0182b976661ead578c3e656
--- /dev/null
+++ b/src/bot/handlers/basic/currency_converter.rs
@@ -0,0 +1,176 @@
+use std::{collections::HashMap, time::Duration};
+
+use grammers_client::{Client, InputMessage, Update};
+use regex::Regex;
+
+use crate::{
+    bot::{filters::base::Filter, handlers::Handler},
+    utils::messages::get_message,
+};
+
+lazy_static::lazy_static! {
+    static ref SUPPORTED_CURS: Vec<&'static str> = vec![
+        "GBP",
+        "HUF",
+        "USD",
+        "EUR",
+        "CNY",
+        "NOK",
+        "UAH",
+        "SEK",
+        "CHF",
+        "KRW",
+        "JPY",
+        "KZT",
+        "PLN",
+        "TRY",
+        "AMD",
+        "RSD",
+    ];
+
+    static ref CONVERTION_ALIASES: HashMap<&'static str, &'static str> = HashMap::from(
+        [
+            // GBP
+            ("фунт", "GBP"),
+            // USD
+            ("бакс", "USD"),
+            ("доллар", "USD"),
+            // EUR
+            ("евро", "EUR"),
+            // JPY
+            ("иен", "JPY"),
+            ("йен", "JPY"),
+            // KRW
+            ("вон", "KRW"),
+            // CHF
+            ("франк", "CHF"),
+            // SEK
+            ("крон", "CHF"),
+            // CNY
+            ("юан", "CNY"),
+            // UAH
+            ("гривна", "UAH"),
+            ("гривны", "UAH"),
+            ("гривен", "UAH"),
+            ("грiвен", "UAH"),
+            // KZT
+            ("тенге", "KZT"),
+            ("тэнге", "KZT"),
+            // PLN
+            ("злот", "PLN"),
+            // TRY
+            ("лир", "TRY"),
+            // AMD
+            ("драм", "AMD"),
+            // RSD
+            ("динар", "RSD"),
+        ]
+    );
+
+    static ref CUR_REGEX: Regex = {
+        #[allow(clippy::clone_double_ref)]
+        let a = CONVERTION_ALIASES.keys()
+            .copied()
+            .chain(SUPPORTED_CURS.iter().copied())
+            .collect::<Vec<_>>()
+            .join("|");
+        Regex::new(format!(r"\s*(?P<cur_value>\d+([\.,]\d+)?)\s+(?P<cur_name>{a})").as_str()).unwrap()
+    };
+}
+
+#[derive(Clone)]
+pub struct CurrencyTextFilter;
+
+#[derive(Clone)]
+pub struct CurrencyConverter {
+    client: reqwest::Client,
+}
+
+impl CurrencyConverter {
+    pub fn new() -> anyhow::Result<Self> {
+        let client = reqwest::ClientBuilder::new()
+            .timeout(Duration::from_secs(2))
+            .gzip(true)
+            .build()?;
+        Ok(Self { client })
+    }
+}
+
+/// This filter check if the message matches regex for currencies.
+impl Filter for CurrencyTextFilter {
+    fn filter(&self, update: &Update) -> anyhow::Result<bool> {
+        let Some(message) = get_message(update) else {
+            return Ok(false);
+        };
+        Ok(CUR_REGEX.is_match(message.text()))
+    }
+}
+
+#[async_trait::async_trait]
+impl Handler for CurrencyConverter {
+    async fn react(&self, _: &Client, update: &Update) -> anyhow::Result<()> {
+        let Some(message) = get_message(update) else{ return  Ok(())};
+        let response = self
+            .client
+            .get("https://www.cbr-xml-daily.ru/daily_json.js")
+            .send()
+            .await?
+            .error_for_status()?
+            .json::<serde_json::Value>()
+            .await?;
+
+        let Some(valutes) = response
+            .get("Valute")
+            .and_then(serde_json::Value::as_object) else{
+                log::warn!("Can't get valutes fom response.");
+                return Ok(());
+            };
+
+        let mut calucates = Vec::new();
+
+        for capture in CUR_REGEX.captures_iter(message.text()) {
+            // We parse supplied value from message
+            let Some(num_value) = capture
+                .name("cur_value")
+                // Convert match to string.
+                .map(|mtch| mtch.as_str())
+                // Parse it.
+                .and_then(|val| val.parse::<f64>().ok()) else{
+                    continue;
+                };
+            let cur_name = capture.name("cur_name").map(|mtch| mtch.as_str());
+            let Some(cur_name) = cur_name
+                // We check if the value is an alias.
+                .and_then(|val| CONVERTION_ALIASES.get(val).copied())
+                // get previous value if not.
+                .or(cur_name) else{
+                    continue;
+                };
+            let calculated = valutes
+                .get(cur_name)
+                .and_then(|info| info.get("Value"))
+                .map(ToString::to_string)
+                .and_then(|value| value.as_str().parse::<f64>().ok())
+                .map(|multiplier| multiplier * num_value);
+            if let Some(value) = calculated {
+                calucates.push(format!(
+                    "<pre>{num_value} {cur_name} = {value:.2} RUB</pre><br>"
+                ));
+            }
+        }
+
+        if !calucates.is_empty() {
+            let mut bot_response =
+                String::from("<b>Полагаясь на текущий курс валют могу сказать следующее:</b>\n\n");
+            for calc in calucates {
+                bot_response.push_str(calc.as_str());
+            }
+            message
+                // We send it as silent, so we can filter this message later.
+                .reply(InputMessage::html(bot_response).silent(true))
+                .await?;
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/bot/handlers/basic/help.rs b/src/bot/handlers/basic/help.rs
index 5f96340ef85e008ef47d860e8eeae5d4f61a5d39..00a96ca9f4b5264a00648b40fbed5f3e4cfe3b50 100644
--- a/src/bot/handlers/basic/help.rs
+++ b/src/bot/handlers/basic/help.rs
@@ -10,7 +10,7 @@ impl Handler for Help {
     async fn react(&self, _: &Client, update: &Update) -> anyhow::Result<()> {
         let Update::NewMessage(message) = update else {return  Ok(())};
 
-        message.reply("Хелпа").await?;
+        message.reply("Я больше не рассказываю что я умею.").await?;
 
         Ok(())
     }
diff --git a/src/bot/handlers/basic/mod.rs b/src/bot/handlers/basic/mod.rs
index c1ee8d4df2f7fe224628d91ecd5304ece7ab33f4..3f81a214a474dcf573fca1e475444a68aa3e2012 100644
--- a/src/bot/handlers/basic/mod.rs
+++ b/src/bot/handlers/basic/mod.rs
@@ -1,2 +1,3 @@
+pub mod currency_converter;
 pub mod get_chat_id;
 pub mod help;
diff --git a/src/bot/handlers/fun/blyaficator.rs b/src/bot/handlers/fun/blyaficator.rs
index 6a4ce8341a87c40e8c354665ac06e95411595889..c8660f897be6a983b92788f7a646a733bdc22b2c 100644
--- a/src/bot/handlers/fun/blyaficator.rs
+++ b/src/bot/handlers/fun/blyaficator.rs
@@ -6,6 +6,7 @@ use grammers_client::{Client, Update};
 
 const BLYA_WORDS: &[&str] = &[", бля,", ", сука,", ", ёбаный рот,", ", охуеть конечно,"];
 
+/// It's time to add some бляs.
 #[derive(Clone)]
 pub struct Blyaficator;
 
diff --git a/src/bot/handlers/fun/greeter.rs b/src/bot/handlers/fun/greeter.rs
index b66c68536dfaffc03c1162506aa98b3b5ac40552..fc83bd3314217f75bf3a6ebef547cb15f52e6f59 100644
--- a/src/bot/handlers/fun/greeter.rs
+++ b/src/bot/handlers/fun/greeter.rs
@@ -4,6 +4,17 @@ use rand::seq::IteratorRandom;
 
 use crate::bot::handlers::base::Handler;
 
+lazy_static::lazy_static! {
+    static ref GREETINGS: &'static [&'static str] = &[
+        "Привет!",
+        "Добрый день!",
+        "Здравствуйте.",
+        "Приетствую.",
+        "Доброго времени суток.",
+    ];
+}
+
+/// Greeter just replies to greeting messages.
 #[derive(Clone)]
 pub struct Greeter;
 
@@ -18,9 +29,8 @@ impl Handler for Greeter {
             return Ok(());
         }
 
-        let reply_text = ["Привет!", "Добрый день!", "Здравствуйте.", "Приетствую"]
-            .into_iter()
-            .choose(&mut rand::thread_rng());
+        // Choose random greeting from the list of greetings.
+        let reply_text = GREETINGS.iter().choose(&mut rand::thread_rng()).copied();
 
         if let Some(text) = reply_text {
             message.reply(text).await?;
diff --git a/src/bot/main.rs b/src/bot/main.rs
index 284f666c45bb567fc64eb7909d0b86c73586e673..c05bb8decfd4460c04ad19e92c560de8e952e361 100644
--- a/src/bot/main.rs
+++ b/src/bot/main.rs
@@ -8,19 +8,30 @@ use tokio::sync::RwLock;
 
 use super::{
     filters::{
-        chain::FilteredHandler,
+        filtered_handler::FilteredHandler,
         message_fitlers::{
-            ExcludedChatsFilter, MessageDirection, MessageDirectionFilter, TextFilter,
-            TextMatchMethod,
+            ExcludedChatsFilter, MessageDirection, MessageDirectionFilter, SilentFilter,
+            TextFilter, TextMatchMethod,
         },
     },
     handlers::{
-        basic::{get_chat_id::GetChatId, help::Help},
+        basic::{
+            currency_converter::{CurrencyConverter, CurrencyTextFilter},
+            get_chat_id::GetChatId,
+            help::Help,
+        },
         fun::{blyaficator::Blyaficator, greeter::Greeter},
         Handler,
     },
 };
 
+/// Authorization function.
+///
+/// This function asks for login code and
+/// waits for it to become available.
+///
+/// Also it validates two-factor authentication
+/// password if it was supplied.
 async fn authorize(
     args: &BotConfig,
     client: &Client,
@@ -32,18 +43,19 @@ async fn authorize(
         .await?;
     let mut code = None;
 
+    // Check for code to becom available every second.
     while code.is_none() {
         tokio::time::sleep(Duration::from_secs(1)).await;
         {
             code = web_code.read().await.clone();
         }
     }
-
+    // Acutal signing in.
     let signed_in = client.sign_in(&token, &code.unwrap()).await;
     match signed_in {
+        // If signing i
         Err(SignInError::PasswordRequired(password_token)) => {
-            // Note: this `prompt` method will echo the password in the console.
-            //       Real code might want to use a better way to handle this.
+            // If the password was not supplied, we use the hint in panic.
             let hint = password_token.hint().unwrap_or("None");
             let password = args
                 .tfa_password
@@ -60,48 +72,82 @@ async fn authorize(
     Ok(())
 }
 
+/// This little function is used to execute handlers on updates and print errors
+/// if something bad happens.
+///
+/// The reason, I created a separate function is simple. I spawn every handler as a
+/// separate task and I don't care if fails.
 async fn handle_with_log(handler: Box<dyn Handler>, client: Client, update_data: Update) {
     if let Err(err) = handler.react(&client, &update_data).await {
         log::error!("{err}");
     }
 }
 
-async fn run(args: BotConfig, client: Client) {
+/// Acutal logic on handling updates.
+///
+/// This function handles every update we get from telegram
+/// and spawns correcsponding handlers.
+///
+/// Also, every available handler is defined here.
+async fn run(args: BotConfig, client: Client) -> anyhow::Result<()> {
     let handlers: Vec<FilteredHandler> = vec![
+        // Printing help.
+        FilteredHandler::new(Help).add_filter(TextFilter(&[".h"], TextMatchMethod::IMatches)),
+        // Greeting my fellow humans.
         FilteredHandler::new(Greeter)
+            .add_filter(SilentFilter)
             .add_filter(MessageDirectionFilter(MessageDirection::Incoming))
             .add_filter(TextFilter(&["привет"], TextMatchMethod::IStartsWith))
             .add_filter(ExcludedChatsFilter(args.excluded_chats)),
-        FilteredHandler::new(Help).add_filter(TextFilter(&[".h"], TextMatchMethod::IMatches)),
+        // Getting chat id.
         FilteredHandler::new(GetChatId)
             .add_filter(TextFilter(&[".cid"], TextMatchMethod::IMatches)),
+        // Make бля fun again.
         FilteredHandler::new(Blyaficator)
             .add_filter(TextFilter(&[".bl"], TextMatchMethod::IStartsWith)),
+        // Handler for converting currecies.
+        FilteredHandler::new(CurrencyConverter::new()?)
+            .add_filter(SilentFilter)
+            .add_filter(ExcludedChatsFilter(args.currency_excluded_chats))
+            .add_filter(CurrencyTextFilter),
     ];
 
     loop {
+        // Get new update
         let update = client.next_update().await;
         if update.is_err() {
             log::error!("{}", update.unwrap_err());
             break;
         }
-        if let Some(update_data) = update.unwrap() {
-            let update_ref = &update_data;
-            let matched_handlers = handlers
-                .par_iter()
-                .filter(move |val| val.check(update_ref))
-                .collect::<Vec<_>>();
-            for handler in matched_handlers {
-                tokio::spawn(handle_with_log(
-                    handler.handler.clone(),
-                    client.clone(),
-                    update_data.clone(),
-                ));
-            }
+        // We get update if there's no error
+        let Some(update_data) = update.ok().and_then(|inner|inner) else{
+            log::warn!("Empty update is found.");
+            continue;
+        };
+        // A reference to update, so we can easily move it.
+        let update_ref = &update_data;
+        let filtered = handlers
+            // A parralel iterator over matchers.
+            .par_iter()
+            // Here we get all handlers that match filters.
+            .filter(move |val| val.check(update_ref))
+            // For each matched handler we spawn a new task.
+            .collect::<Vec<_>>();
+        for handler in filtered {
+            tokio::spawn(handle_with_log(
+                handler.handler.clone(),
+                client.clone(),
+                update_data.clone(),
+            ));
         }
     }
+    Ok(())
 }
 
+/// The main entrypoint for bot.
+///
+/// This function starts bot, performs login and
+/// starts endless loop.
 pub async fn start(args: BotConfig, web_code: Arc<RwLock<Option<String>>>) -> anyhow::Result<()> {
     log::info!("Connecting to Telegram...");
     let client = Client::connect(Config {
@@ -115,17 +161,20 @@ pub async fn start(args: BotConfig, web_code: Arc<RwLock<Option<String>>>) -> an
         },
     })
     .await?;
+
     log::info!("Connected!");
+
     if client.is_authorized().await? {
         // If we already authrized, we write random token, so web won't update it.
         let mut code_writer = web_code.write().await;
         *code_writer = Some(String::new());
     } else {
+        // If we don't have token, wait for it.
         authorize(&args, &client, web_code).await?;
         client.session().save_to_file(args.session_file.as_str())?;
     }
 
-    run(args.clone(), client).await;
+    run(args.clone(), client).await?;
 
     Ok(())
 }
diff --git a/src/main.rs b/src/main.rs
index 1d4ed0d8f8bead92de0d5f527789f8576ebede54..060f508fd25c1cbfdac39ab0876772f5ffe9ca3b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -25,27 +25,27 @@ async fn main() -> anyhow::Result<()> {
 
     let token_lock = Arc::new(RwLock::new(None));
 
-    let bot_token = token_lock.clone();
-    let server_token = token_lock.clone();
-
-    let web_server = server::create(args.server.clone(), server_token)?;
-    let bot_future = bot::start(args.bot.clone(), bot_token);
-
-    let tasks = [
-        tokio::task::spawn(bot_future),
-        tokio::task::spawn(error_wrap(web_server)),
-    ];
-
-    let completed = tasks
-        .into_iter()
-        .collect::<FuturesUnordered<_>>()
-        .take(1)
-        .collect::<Vec<_>>()
-        .await;
-
-    if let Some(fut) = completed.into_iter().next() {
-        fut?
-    } else {
-        Ok(())
-    }
+    [
+        // Spawining bot task
+        tokio::task::spawn(bot::start(args.bot.clone(), token_lock.clone())),
+        // Spawning server task.
+        tokio::task::spawn(error_wrap(server::create(
+            args.server.clone(),
+            token_lock.clone(),
+        )?)),
+    ]
+    .into_iter()
+    // Turning all tasks in unirdered futures set.
+    .collect::<FuturesUnordered<_>>()
+    // Grab first completed future
+    .take(1)
+    // Take the value
+    .next()
+    // Await for it to complete
+    .await
+    // Unwrap (since we can guarantee that it's not empty).
+    // Throw all errors by using ??. First for joining task, second from the task itself.
+    .unwrap()??;
+
+    Ok(())
 }
diff --git a/src/server/main.rs b/src/server/main.rs
index 8559ea06af58f2075725f79d7092f43db2af1033..e17691efc19807472bee0d6f08efcef1d123d12a 100644
--- a/src/server/main.rs
+++ b/src/server/main.rs
@@ -11,9 +11,10 @@ pub fn create(args: ServerConfig, token: Arc<RwLock<Option<String>>>) -> anyhow:
     let addr = (args.host.clone(), args.port);
     let server = HttpServer::new(move || {
         App::new()
-            .wrap(actix_web::middleware::Logger::new(
-                "\"%r\" \"-\" \"%s\" \"%a\" \"%D\"",
-            ))
+            .wrap(
+                actix_web::middleware::Logger::new("\"%r\" \"-\" \"%s\" \"%a\" \"%D\"")
+                    .exclude("/health"),
+            )
             .app_data(Data::new(token.clone()))
             .app_data(Data::new(args.clone()))
             .service(login)
diff --git a/src/server/templates/mod.rs b/src/server/templates/mod.rs
index d0d2b26268e0443b406bdf1576130bec300228a7..f6b9e03d439c13accf95a188f9640c68633ac18f 100644
--- a/src/server/templates/mod.rs
+++ b/src/server/templates/mod.rs
@@ -1,5 +1,16 @@
 use askama::Template;
 
+/// Index pages.
+///
+/// This page is used to authenticate users.
+/// It has two states: activated or not.
+///
+/// It the activated is false, we
+/// render two input fields, with telegram code
+/// and server's password.
+///
+/// If the user is authenticated, we just render
+/// some random information.
 #[derive(Template)]
 #[template(path = "index.html")]
 pub struct Index {