├── .gitignore ├── .rspec ├── Procfile ├── screenshots ├── start.png ├── country.png ├── location.png └── countries.png ├── bin └── main.rb ├── Gemfile ├── spec ├── bot_spec.rb ├── covid_ap_spec.rb └── spec_helper.rb ├── .stickler.yml ├── .github └── workflows │ └── linters.yml ├── .rubocop.yml ├── Gemfile.lock ├── lib ├── covid_api.rb └── bot.rb └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .Gemfile.lock 2 | .env -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | bot: ruby ./bin/main.rb -------------------------------------------------------------------------------- /screenshots/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/santiagorodriguezbermudez/Telegram-Covid-Bot/HEAD/screenshots/start.png -------------------------------------------------------------------------------- /screenshots/country.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/santiagorodriguezbermudez/Telegram-Covid-Bot/HEAD/screenshots/country.png -------------------------------------------------------------------------------- /screenshots/location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/santiagorodriguezbermudez/Telegram-Covid-Bot/HEAD/screenshots/location.png -------------------------------------------------------------------------------- /screenshots/countries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/santiagorodriguezbermudez/Telegram-Covid-Bot/HEAD/screenshots/countries.png -------------------------------------------------------------------------------- /bin/main.rb: -------------------------------------------------------------------------------- 1 | require 'telegram/bot' 2 | require 'dotenv' 3 | require_relative '../lib/bot.rb' 4 | 5 | Dotenv.load 6 | 7 | Bot.new 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 4 | 5 | gem 'dotenv' 6 | gem 'geocoder' 7 | gem 'rspec' 8 | gem 'rubocop' 9 | gem 'telegram-bot-ruby' 10 | gem 'uri' 11 | -------------------------------------------------------------------------------- /spec/bot_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/bot.rb' 2 | 3 | describe Bot do 4 | let(:bot) { Bot.new } 5 | 6 | describe '#initialize' do 7 | it 'Starts the Bot without error' do 8 | expect(bot.class).to eql Bot 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /.stickler.yml: -------------------------------------------------------------------------------- 1 | # add the linters you want stickler to use for this project 2 | linters: 3 | rubocop: 4 | display_cop_names: true 5 | # indicate where is the config file for stylelint 6 | config: "./rubocop.yml" 7 | ​ 8 | files: 9 | ignore: 10 | - "Guardfile" 11 | - "Rakefile" 12 | - "node_modules/**/*" 13 | 14 | # PLEASE DO NOT enable auto fixing options 15 | # if you need extra support from you linter - do it in your local env as described in README for this config 16 | # find full documentation here: https://stickler-ci.com/docs -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | rubocop: 7 | name: Rubocop 8 | runs-on: ubuntu-18.04 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-ruby@v1 12 | with: 13 | ruby-version: 2.6.x 14 | - name: Setup Rubocop 15 | run: | 16 | gem install --no-document rubocop:'~>0.81.0' # https://docs.rubocop.org/en/stable/installation/ 17 | [ -f .rubocop.yml ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/ruby/.rubocop.yml 18 | - name: Rubocop Report 19 | run: rubocop --color -------------------------------------------------------------------------------- /spec/covid_ap_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/covid_api' 2 | require 'geocoder' 3 | 4 | describe CovidApi do 5 | let(:api) { CovidApi.new } 6 | 7 | describe '#country' do 8 | let(:array) { api.countries } 9 | 10 | it 'Returns the selected country when it is given' do 11 | array.each do |el| 12 | text_output = api.country(el) 13 | p text_output 14 | expect((text_output.include? 'has no data on Api')).to eql(false) 15 | end 16 | end 17 | 18 | it 'Returns a String even when given a location that does not have any match' do 19 | array.each do |slug| 20 | slug = slug.split('-').map(&:capitalize).join(' ') 21 | location = Geocoder.search(slug) 22 | p slug 23 | expect(api.country(api.get_slug_country(location.first.country)).class).to eql(String) unless location == [] 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - "Guardfile" 4 | - "Rakefile" 5 | 6 | DisplayCopNames: true 7 | 8 | Layout/LineLength: 9 | Max: 120 10 | Metrics/MethodLength: 11 | Max: 20 12 | Metrics/AbcSize: 13 | Max: 50 14 | Metrics/ClassLength: 15 | Max: 150 16 | Metrics/BlockLength: 17 | ExcludedMethods: ['describe'] 18 | Max: 30 19 | 20 | 21 | Style/Documentation: 22 | Enabled: false 23 | Style/ClassAndModuleChildren: 24 | Enabled: false 25 | Style/EachForSimpleLoop: 26 | Enabled: false 27 | Style/AndOr: 28 | Enabled: false 29 | Style/DefWithParentheses: 30 | Enabled: false 31 | Style/FrozenStringLiteralComment: 32 | EnforcedStyle: never 33 | 34 | Layout/HashAlignment: 35 | EnforcedColonStyle: key 36 | Layout/ExtraSpacing: 37 | AllowForAlignment: false 38 | Layout/MultilineMethodCallIndentation: 39 | Enabled: true 40 | EnforcedStyle: indented 41 | Lint/RaiseException: 42 | Enabled: false 43 | Lint/StructNewOverride: 44 | Enabled: false 45 | Style/HashEachMethods: 46 | Enabled: false 47 | Style/HashTransformKeys: 48 | Enabled: false 49 | Style/HashTransformValues: 50 | Enabled: false 51 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.0) 5 | axiom-types (0.1.1) 6 | descendants_tracker (~> 0.0.4) 7 | ice_nine (~> 0.11.0) 8 | thread_safe (~> 0.3, >= 0.3.1) 9 | coercible (1.0.0) 10 | descendants_tracker (~> 0.0.1) 11 | descendants_tracker (0.0.4) 12 | thread_safe (~> 0.3, >= 0.3.1) 13 | diff-lcs (1.4.2) 14 | dotenv (2.7.5) 15 | equalizer (0.0.11) 16 | faraday (1.0.1) 17 | multipart-post (>= 1.2, < 3) 18 | geocoder (1.6.3) 19 | ice_nine (0.11.2) 20 | inflecto (0.0.2) 21 | multipart-post (2.1.1) 22 | parallel (1.19.1) 23 | parser (2.7.1.3) 24 | ast (~> 2.4.0) 25 | rainbow (3.0.0) 26 | regexp_parser (1.7.1) 27 | rexml (3.2.4) 28 | rspec (3.9.0) 29 | rspec-core (~> 3.9.0) 30 | rspec-expectations (~> 3.9.0) 31 | rspec-mocks (~> 3.9.0) 32 | rspec-core (3.9.2) 33 | rspec-support (~> 3.9.3) 34 | rspec-expectations (3.9.2) 35 | diff-lcs (>= 1.2.0, < 2.0) 36 | rspec-support (~> 3.9.0) 37 | rspec-mocks (3.9.1) 38 | diff-lcs (>= 1.2.0, < 2.0) 39 | rspec-support (~> 3.9.0) 40 | rspec-support (3.9.3) 41 | rubocop (0.85.1) 42 | parallel (~> 1.10) 43 | parser (>= 2.7.0.1) 44 | rainbow (>= 2.2.2, < 4.0) 45 | regexp_parser (>= 1.7) 46 | rexml 47 | rubocop-ast (>= 0.0.3) 48 | ruby-progressbar (~> 1.7) 49 | unicode-display_width (>= 1.4.0, < 2.0) 50 | rubocop-ast (0.0.3) 51 | parser (>= 2.7.0.1) 52 | ruby-progressbar (1.10.1) 53 | telegram-bot-ruby (0.12.0) 54 | faraday 55 | inflecto 56 | virtus 57 | thread_safe (0.3.6) 58 | unicode-display_width (1.7.0) 59 | uri (0.10.0) 60 | virtus (1.0.5) 61 | axiom-types (~> 0.1) 62 | coercible (~> 1.0) 63 | descendants_tracker (~> 0.0, >= 0.0.3) 64 | equalizer (~> 0.0, >= 0.0.9) 65 | 66 | PLATFORMS 67 | ruby 68 | 69 | DEPENDENCIES 70 | dotenv 71 | geocoder 72 | rspec 73 | rubocop 74 | telegram-bot-ruby 75 | uri 76 | 77 | BUNDLED WITH 78 | 1.17.2 79 | -------------------------------------------------------------------------------- /lib/covid_api.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'net/http' 3 | require 'json' 4 | 5 | class CovidApi 6 | attr_reader :commands 7 | def initialize 8 | @url = 'https://api.covid19api.com/' 9 | end 10 | 11 | def summary 12 | organize_output(get_information('summary') { |hash| hash['Global'] }) 13 | end 14 | 15 | def countries 16 | country_array = get_information('summary') { |hash| hash['Countries'] } 17 | country_array.map! { |el| el['Slug'] } 18 | country_array.sort 19 | end 20 | 21 | def country(country) 22 | country_hash = get_information('total/country/' + country) { |arr| arr[arr.length - 1] } if country.ascii_only? 23 | 24 | if country == 'Error' 25 | 'There is no data for this location available, try using the slug name.' 26 | elsif country_hash 27 | 28 | selected_country_fields = country_hash.select { |k, _| %w[Confirmed Country Deaths Recovered Active].include?(k) } 29 | 30 | text_output = "Country: #{selected_country_fields['Country']}\n" 31 | 32 | text_output += organize_output(selected_country_fields.reject { |k, _| k == 'Country' }) 33 | text_output 34 | else 35 | organize_output(country.split('-').join(' ').capitalize + ' has no data on Api') 36 | end 37 | end 38 | 39 | def get_slug_country(input_country) 40 | country_array = get_information('summary') { |hash| hash['Countries'] } 41 | country_array_with_slug = country_array.map { |h| h.select { |k, _| %w[Slug Country].include?(k) } } 42 | index = country_array_with_slug.find_index { |el| el['Country'].downcase == input_country.downcase } 43 | if index 44 | country_array[index]['Slug'] 45 | else 46 | 'Error' 47 | end 48 | end 49 | 50 | private 51 | 52 | def get_information(path) 53 | url_path = URI(@url + path) 54 | https = Net::HTTP.new(url_path.host, url_path.port) 55 | https.use_ssl = true 56 | 57 | request = Net::HTTP::Get.new(url_path) 58 | response = https.request(request) 59 | summary_hash = JSON.parse(response.read_body) 60 | 61 | yield(summary_hash) 62 | end 63 | 64 | def organize_output(object) 65 | return object if object.is_a? String 66 | 67 | text_output = '' 68 | object.each do |key, value| 69 | value = value.to_s.reverse.scan(/\d{1,3}/).join(',').reverse 70 | key = key.split(/(?=[A-Z])/).join(' ') 71 | text_output += "#{key}: #{value}\n" 72 | end 73 | text_output 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 16 | RSpec.configure do |config| 17 | # rspec-expectations config goes here. You can use an alternate 18 | # assertion/expectation library such as wrong or the stdlib/minitest 19 | # assertions if you prefer. 20 | config.expect_with :rspec do |expectations| 21 | # This option will default to `true` in RSpec 4. It makes the `description` 22 | # and `failure_message` of custom matchers include text for helper methods 23 | # defined using `chain`, e.g.: 24 | # be_bigger_than(2).and_smaller_than(4).description 25 | # # => "be bigger than 2 and smaller than 4" 26 | # ...rather than: 27 | # # => "be bigger than 2" 28 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 29 | end 30 | 31 | # rspec-mocks config goes here. You can use an alternate test double 32 | # library (such as bogus or mocha) by changing the `mock_with` option here. 33 | config.mock_with :rspec do |mocks| 34 | # Prevents you from mocking or stubbing a method that does not exist on 35 | # a real object. This is generally recommended, and will default to 36 | # `true` in RSpec 4. 37 | mocks.verify_partial_doubles = true 38 | end 39 | 40 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 41 | # have no way to turn it off -- the option exists only for backwards 42 | # compatibility in RSpec 3). It causes shared context metadata to be 43 | # inherited by the metadata hash of host groups and examples, rather than 44 | # triggering implicit auto-inclusion in groups with matching metadata. 45 | config.shared_context_metadata_behavior = :apply_to_host_groups 46 | 47 | # The settings below are suggested to provide a good initial experience 48 | # with RSpec, but feel free to customize to your heart's content. 49 | end 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Covid -> Your daily Covid update news 2 | 3 | > Covid provides the latest data on how the Pandemia has evolved globally and by country over/ All through Telegram. 4 | 5 | Provide the latest data from the Pandemic situation worldwide. [Start talking with Covid.](https://t.me/mv_covid_bot) 6 | 7 | ## Table of Contents 8 | 9 | * [Getting Started](#getting-started) 10 | * [About the Project](#about-the-project) 11 | * [Built With](#built-with) 12 | * [Acknowledgements](#acknowledgements) 13 | * [License](#license) 14 | * [Contact](#contact) 15 | 16 | 17 | ## Getting Started 18 | You can chat directly with the bot by searching for @mv_covid_bot on Telegram. [Click here.](https://t.me/mv_covid_bot)) 19 | 20 | ### Prerequisites 21 | To install this project on your own and create another bot with the same logic, please be sure to install and use: 22 | 23 | - Ruby, 24 | - Gems within Ruby, 25 | - Heroku for deployment, 26 | - Rspec for running tests. 27 | 28 | ### Install 29 | - Fork this project into your local machine. 30 | - Create a [bot token here](https://core.telegram.org/bots#6-botfather) and include it on a .env file. Save the variable as 'TOKEN'. 31 | - Open your project directory on your terminal 32 | - Install gems by running "bundle install" on your terminal 33 | - Run 'ruby bin/main.rb' on your terminal. 34 | - Open Telegram and start talking with @mv_covid_bot 35 | 36 | ### Usage 37 | - The bot generates a main command menu by writing /start 38 | - Follow the instructions. 39 | 40 | ## Commands 41 | 42 | ### Start 43 | Provide the main menu and the recent global numbers. 44 | ![/start](./screenshots/start.png) 45 | 46 | ### Send your location 47 | Share your location to get the stats from your country 48 | ![Share location](./screenshots/location.png) 49 | 50 | ### See list of countries 51 | Select the 'Select a specific country' to get a list of all of the current countries from which you can get data. 52 | ![List of Countries](./screenshots/countries.png) 53 | 54 | ### Type a specific country 55 | Type a country from the list and see its stats up to date. 56 | ![Search a country](./screenshots/country.png) 57 | 58 | 59 | ## About The Project 60 | This is a educational project as part of the Microverse Curriculum. 61 | 62 | ## Built With 63 | 64 | - Ruby version 2.6.5, 65 | - Telegram/bot 66 | - [Covid API](https://covid19api.com/) 67 | - Rspec 68 | 69 | ## Tests 70 | - Run rspec for testing bot and covid_api classes. This will mainly test that each country has the data with no problem. 71 | 72 | ## Authors 73 | 74 | 👤 **Santiago Rodriguez** 75 | 76 | - Github: [@santiagorodriguezbermudez](https://github.com/santiagorodriguezbermudez) 77 | - Twitter: [@srba87](https://twitter.com/srba87) 78 | - Linkedin: [@srba87](https://www.linkedin.com/in/srba87/) 79 | - Email: [srba87@gmail.com](srba87@gmail.com) 80 | 81 | ## 🤝 Contributing 82 | 83 | Contributions, issues and feature requests are welcome! 84 | 85 | Feel free to check the [issues page](issues/). 86 | 87 | ## Show your support 88 | 89 | Give a ⭐️ if you like this project! 90 | 91 | ## Acknowledgments 92 | 93 | - Special thanks to The Ocicats team from Microverse! 94 | - Maria Reyes for making the suggestion to work with Telegram 95 | - Ruby gem: [Telegram Bot Ruby](https://github.com/atipugin/telegram-bot-ruby) 96 | 97 | ## 📝 License 98 | 99 | Bot Icon of the virus by Arya Icons from the Noun Project 100 | This project is [MIT](lic.url) licensed. 101 | -------------------------------------------------------------------------------- /lib/bot.rb: -------------------------------------------------------------------------------- 1 | require 'telegram/bot' 2 | require 'geocoder' 3 | require_relative 'covid_api' 4 | 5 | class Bot 6 | attr_reader :commands, :token 7 | 8 | def initialize 9 | @token = ENV['TOKEN'] 10 | begin 11 | start_telegram_api 12 | rescue Telegram::Bot::Exceptions::ResponseError => e 13 | puts "Bot not connecting properly. Presenting: #{e}" 14 | end 15 | end 16 | 17 | # Replies messages to the user 18 | def reply(bot, chat_id, content, markup = nil) 19 | bot.api.send_message(chat_id: chat_id, text: content, reply_markup: markup) 20 | end 21 | 22 | private 23 | 24 | # Starts the bot input. 25 | def start_telegram_api 26 | Telegram::Bot::Client.run(token) do |bot| 27 | listen(bot) 28 | end 29 | end 30 | 31 | # Listens for user input 32 | def listen(bot) 33 | bot.listen do |message| 34 | case message 35 | when Telegram::Bot::Types::Message 36 | listen_message_text(bot, message) 37 | 38 | when Telegram::Bot::Types::CallbackQuery 39 | case message.data 40 | when 'location' 41 | reply(bot, message.from.id, 'Please provide me your location...', inline_menu) 42 | 43 | when 'countries' 44 | reply(bot, message.from.id, 'Please type one of the following countries to get information:') 45 | reply(bot, message.from.id, search('countries')) 46 | 47 | else 48 | reply(bot, message.from.id, "I don't know how to help you with this") 49 | end 50 | end 51 | end 52 | end 53 | 54 | def listen_message_text(bot, message) 55 | if message.text == '/start' 56 | reply(bot, message.chat.id, "Hello, #{message.from.first_name}.") 57 | reply(bot, 58 | message.chat.id, 59 | "This is the latest update on Covid for #{Date.today.strftime('%a, %-d %b of %Y:')}") 60 | reply(bot, message.chat.id, search('/start')) 61 | reply(bot, message.chat.id, 'Please select one of the following options', main_menu) 62 | 63 | elsif message.text.nil? 64 | # Provides stats according to the country if given a location. 65 | location = Geocoder.search([message.location.latitude, message.location.longitude]) 66 | reply(bot, message.chat.id, search('location', location)) 67 | 68 | elsif search('countries').include? message.text.downcase 69 | reply(bot, message.chat.id, search(message.text)) 70 | else 71 | reply(bot, message.chat.id, "I can't help you, please select from the following options:", main_menu) 72 | end 73 | end 74 | 75 | # Connects with the Covid API Class 76 | def search(command, location = nil) 77 | covid_api = CovidApi.new 78 | 79 | case command 80 | when '/start' 81 | covid_api.summary 82 | 83 | when 'countries' 84 | covid_api.countries.join(', ') 85 | 86 | when 'location' 87 | covid_api.country(covid_api.get_slug_country(location.first.country)) if location 88 | 89 | else 90 | covid_api.country(command) 91 | end 92 | end 93 | 94 | # Provides the user with the current options 95 | def main_menu 96 | kb = [ 97 | Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Latest news on Covid', url: 'https://news.google.com/covid19/map?hl=en-US&gl=US&ceid=US:en'), 98 | Telegram::Bot::Types::InlineKeyboardButton.new(text: 'How are the numbers in my location?', 99 | callback_data: 'location'), 100 | Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Select a specific country', callback_data: 'countries') 101 | ] 102 | Telegram::Bot::Types::InlineKeyboardMarkup.new(inline_keyboard: kb) 103 | end 104 | 105 | # Prompts the user to provide its location 106 | def inline_menu 107 | kb = [ 108 | Telegram::Bot::Types::KeyboardButton.new( 109 | text: 'Provide Covid my Location', 110 | request_location: true, 111 | one_time_keyboard: true 112 | ) 113 | ] 114 | Telegram::Bot::Types::ReplyKeyboardMarkup.new(keyboard: kb) 115 | end 116 | end 117 | --------------------------------------------------------------------------------