### # Copyright (c) 2020, Hoaas # Copyright (c) 2020, oddluck # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import json import requests import csv import datetime from supybot import utils, plugins, ircutils, callbacks, log from supybot.commands import * try: from supybot.i18n import PluginInternationalization _ = PluginInternationalization("CoronaLight") except ImportError: # Placeholder that allows to run the plugin on a bot # without the i18n module _ = lambda x: x countries = { "AF": "AFGHANISTAN", "AX": "ÅLAND ISLANDS", "AL": "ALBANIA", "DZ": "ALGERIA", "AS": "AMERICAN SAMOA", "AD": "ANDORRA", "AO": "ANGOLA", "AI": "ANGUILLA", "AQ": "ANTARCTICA", "AG": "ANTIGUA AND BARBUDA", "AR": "ARGENTINA", "AM": "ARMENIA", "AW": "ARUBA", "AU": "AUSTRALIA", "AT": "AUSTRIA", "AZ": "AZERBAIJAN", "BS": "BAHAMAS, THE", "BH": "BAHRAIN", "BD": "BANGLADESH", "BB": "BARBADOS", "BY": "BELARUS", "BE": "BELGIUM", "BZ": "BELIZE", "BJ": "BENIN", "BM": "BERMUDA", "BT": "BHUTAN", "BO": "BOLIVIA", "BQ": "BONAIRE", "BA": "BOSNIA AND HERZEGOVINA", "BW": "BOTSWANA", "BV": "BOUVET ISLAND", "BR": "BRAZIL", "IO": "BRITISH INDIAN OCEAN TERRITORY", "BN": "BRUNEI", "BG": "BULGARIA", "BF": "BURKINA FASO", "BI": "BURUNDI", "KH": "CAMBODIA", "CM": "CAMEROON", "CA": "CANADA", "CV": "CAPE VERDE", "KY": "CAYMAN ISLANDS", "CF": "CENTRAL AFRICAN REPUBLIC", "TD": "CHAD", "CL": "CHILE", "CN": "CHINA", "CX": "CHRISTMAS ISLAND", "CC": "COCOS ISLANDS", "CO": "COLOMBIA", "KM": "COMOROS", "CG": "CONGO (BRAZZAVILLE)", "CD": "CONGO (KINSHASA)", "CK": "COOK ISLANDS", "CR": "COSTA RICA", "CI": "CÔTE D'IVOIRE", "HR": "CROATIA", "CU": "CUBA", "CW": "CURAÇAO", "CY": "CYPRUS", "CZ": "CZECHIA", "DK": "DENMARK", "DJ": "DJIBOUTI", "DM": "DOMINICA", "DO": "DOMINICAN REPUBLIC", "EC": "ECUADOR", "EG": "EGYPT", "SV": "EL SALVADOR", "GQ": "EQUATORIAL GUINEA", "ER": "ERITREA", "EE": "ESTONIA", "ET": "ETHIOPIA", "FK": "FALKLAND ISLANDS", "FO": "FAROE ISLANDS", "FJ": "FIJI", "FI": "FINLAND", "FR": "FRANCE", "GF": "GUIANA", "PF": "POLYNESIA", "TF": "FRENCH SOUTHERN TERRITORIES", "GA": "GABON", "GM": "GAMBIA, THE", "GE": "GEORGIA", "DE": "GERMANY", "GH": "GHANA", "GI": "GIBRALTAR", "GR": "GREECE", "GL": "GREENLAND", "GD": "GRENADA", "GP": "GUADELOUPE", "GU": "GUAM", "GT": "GUATEMALA", "GG": "GUERNSEY", "GN": "GUINEA", "GW": "GUINEA-BISSAU", "GY": "GUYANA", "HT": "HAITI", "HM": "HEARD ISLAND AND MCDONALD ISLANDS", "VA": "HOLY SEE", "HN": "HONDURAS", "HK": "HONG KONG", "HU": "HUNGARY", "IS": "ICELAND", "IN": "INDIA", "ID": "INDONESIA", "IR": "IRAN", "IQ": "IRAQ", "IE": "IRELAND", "IM": "ISLE OF MAN", "IL": "ISRAEL", "IT": "ITALY", "JM": "JAMAICA", "JP": "JAPAN", "JE": "JERSEY", "JO": "JORDAN", "KZ": "KAZAKHSTAN", "KE": "KENYA", "KI": "KIRIBATI", "KP": "KOREA, NORTH", "KR": "KOREA, SOUTH", "KW": "KUWAIT", "KG": "KYRGYZSTAN", "LA": "LAOS", "LV": "LATVIA", "LB": "LEBANON", "LS": "LESOTHO", "LR": "LIBERIA", "LY": "LIBYA", "LI": "LIECHTENSTEIN", "LT": "LITHUANIA", "LU": "LUXEMBOURG", "MO": "MACAO", "MK": "NORTH MACEDONIA", "MG": "MADAGASCAR", "MW": "MALAWI", "MY": "MALAYSIA", "MV": "MALDIVES", "ML": "MALI", "MT": "MALTA", "MH": "MARSHALL ISLANDS", "MQ": "MARTINIQUE", "MR": "MAURITANIA", "MU": "MAURITIUS", "YT": "MAYOTTE", "MX": "MEXICO", "FM": "MICRONESIA", "MD": "MOLDOVA", "MC": "MONACO", "MN": "MONGOLIA", "ME": "MONTENEGRO", "MS": "MONTSERRAT", "MA": "MOROCCO", "MZ": "MOZAMBIQUE", "MM": "MYANMAR", "NA": "NAMIBIA", "NR": "NAURU", "NP": "NEPAL", "NL": "NETHERLANDS", "NC": "NEW CALEDONIA", "NZ": "NEW ZEALAND", "NI": "NICARAGUA", "NE": "NIGER", "NG": "NIGERIA", "NU": "NIUE", "NF": "NORFOLK ISLAND", "MP": "NORTHERN MARIANA ISLANDS", "NO": "NORWAY", "OM": "OMAN", "PK": "PAKISTAN", "PW": "PALAU", "PS": "PALESTINE, STATE OF", "PA": "PANAMA", "PG": "PAPUA NEW GUINEA", "PY": "PARAGUAY", "PE": "PERU", "PH": "PHILIPPINES", "PN": "PITCAIRN", "PL": "POLAND", "PT": "PORTUGAL", "PR": "PUERTO RICO", "QA": "QATAR", "RE": "RÉUNION", "RO": "ROMANIA", "RU": "RUSSIA", "RW": "RWANDA", "BL": "SAINT BARTHÉLEMY", "SH": "SAINT HELENA", "KN": "SAINT KITTS AND NEVIS", "LC": "SAINT LUCIA", "MF": "SAINT MARTIN", "PM": "SAINT PIERRE AND MIQUELON", "VC": "SAINT VINCENT AND THE GRENADINES", "WS": "SAMOA", "SM": "SAN MARINO", "ST": "SAO TOME AND PRINCIPE", "SA": "SAUDI ARABIA", "SN": "SENEGAL", "RS": "SERBIA", "SC": "SEYCHELLES", "SL": "SIERRA LEONE", "SG": "SINGAPORE", "SX": "SINT MAARTEN", "SK": "SLOVAKIA", "SI": "SLOVENIA", "SB": "SOLOMON ISLANDS", "SO": "SOMALIA", "ZA": "SOUTH AFRICA", "GS": "GEORGIA", "SS": "SOUTH SUDAN", "ES": "SPAIN", "LK": "SRI LANKA", "SD": "SUDAN", "SR": "SURINAME", "SJ": "SVALBARD AND JAN MAYEN", "SZ": "SWAZILAND", "SE": "SWEDEN", "CH": "SWITZERLAND", "SY": "SYRIA", "TW": "TAIWAN", "TJ": "TAJIKISTAN", "TZ": "TANZANIA", "TH": "THAILAND", "TL": "TIMOR-LESTE", "TG": "TOGO", "TK": "TOKELAU", "TO": "TONGA", "TT": "TRINIDAD AND TOBAGO", "TN": "TUNISIA", "TR": "TURKEY", "TM": "TURKMENISTAN", "TC": "TURKS AND CAICOS ISLANDS", "TV": "TUVALU", "UG": "UGANDA", "UA": "UKRAINE", "AE": "UNITED ARAB EMIRATES", "GB": "UNITED KINGDOM", "UK": "UNITED KINGDOM", "US": "US", "UM": "UNITED STATES MINOR OUTLYING ISLANDS", "UY": "URUGUAY", "UZ": "UZBEKISTAN", "VU": "VANUATU", "VE": "VENEZUELA", "VN": "VIETNAM", "VG": "VIRGIN ISLANDS", "VI": "VIRGIN ISLANDS", "WF": "WALLIS AND FUTUNA", "EH": "WESTERN SAHARA", "YE": "YEMEN", "ZM": "ZAMBIA", "ZW": "ZIMBABWE", } states = { "AK": "ALASKA", "AL": "ALABAMA", "AR": "ARKANSAS", "AS": "AMERICAN SAMOA", "AZ": "ARIZONA", "CA": "CALIFORNIA", "CO": "COLORADO", "CT": "CONNECTICUT", "DC": "DISTRICT OF COLUMBIA", "DE": "DELAWARE", "FL": "FLORIDA", "GA": "GEORGIA", "GU": "GUAM", "HI": "HAWAII", "IA": "IOWA", "ID": "IDAHO", "IL": "ILLINOIS", "IN": "INDIANA", "KS": "KANSAS", "KY": "KENTUCKY", "LA": "LOUISIANA", "MA": "MASSACHUSETTS", "MD": "MARYLAND", "ME": "MAINE", "MI": "MICHIGAN", "MN": "MINNESOTA", "MO": "MISSOURI", "MP": "NORTHERN MARIANA ISLANDS", "MS": "MISSISSIPPI", "MT": "MONTANA", "NA": "NATIONAL", "NC": "NORTH CAROLINA", "ND": "NORTH DAKOTA", "NE": "NEBRASKA", "NH": "NEW HAMPSHIRE", "NJ": "NEW JERSEY", "NM": "NEW MEXICO", "NV": "NEVADA", "NY": "NEW YORK", "OH": "OHIO", "OK": "OKLAHOMA", "OR": "OREGON", "PA": "PENNSYLVANIA", "PR": "PUERTO RICO", "RI": "RHODE ISLAND", "SC": "SOUTH CAROLINA", "SD": "SOUTH DAKOTA", "TN": "TENNESSEE", "TX": "TEXAS", "UT": "UTAH", "VA": "VIRGINIA", "VI": "VIRGIN ISLANDS", "VT": "VERMONT", "WA": "WASHINGTON", "WI": "WISCONSIN", "WV": "WEST VIRGINIA", "WY": "WYOMING", } class CoronaLight(callbacks.Plugin): """Displays current stats of the Coronavirus outbreak""" threaded = True def __init__(self, irc): self.__parent = super(CoronaLight, self) self.__parent.__init__(irc) self.cache = {} def getCSV(self): data = None try: day = datetime.date.today().strftime("%m-%d-%Y") url = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_daily_reports/{0}.csv".format( day ) r = requests.get(url, timeout=10) r.raise_for_status() except ( requests.exceptions.RequestException, requests.exceptions.HTTPError, ) as e: log.debug("Corona: error retrieving data for today: {0}".format(e)) try: day = datetime.date.today() - datetime.timedelta(days=1) day = day.strftime("%m-%d-%Y") url = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_daily_reports/{0}.csv".format( day ) r = requests.get(url, timeout=10) r.raise_for_status() except ( requests.exceptions.RequestException, requests.exceptions.HTTPError, ) as e: log.debug("Corona: error retrieving data for yesterday: {0}".format(e)) else: data = csv.DictReader(r.iter_lines(decode_unicode=True)) else: data = csv.DictReader(r.iter_lines(decode_unicode=True)) return data def getAPI(self): data = None url = "https://services1.arcgis.com/0MSEUqKaxRlEPj5g/arcgis/rest/services/ncov_cases/FeatureServer/1/query?f=json&where=Confirmed>0&outFields=*" try: r = requests.get(url, timeout=10) r.raise_for_status() except ( requests.exceptions.RequestException, requests.exceptions.HTTPError, ) as e: log.debug("Corona: error retrieving data from API: {0}".format(e)) else: try: r = json.loads(r.content.decode()) data = r.get("features") except: data = None if not data: log.debug("Corona: Error retrieving features data from API.") return data def timeCreated(self, time): time = datetime.datetime.fromtimestamp(time / 1000.0) d = datetime.datetime.now() - time if d.days: rel_time = "{:1d}d ago".format(abs(d.days)) elif d.seconds > 3600: rel_time = "{:.1f}h ago".format(round((abs(d.seconds) / 3600), 1)) elif 60 <= d.seconds < 3600: rel_time = "{:.1f}m ago".format(round((abs(d.seconds) / 60), 1)) else: rel_time = "%ss ago" % (abs(d.seconds)) return rel_time @wrap([optional("text")]) def corona(self, irc, msg, args, search): """[region] Displays Coronavirus statistics. Add a region name to search for country/state specific results. Accepts full country/state names or ISO 3166-1 alpha-2 (two character) country abbreviations and US Postal (two character) state abbreviations. Invalid region names or search terms without data return global results. """ git = api = False data = None if len(self.cache) > 0: cacheLifetime = self.registryValue("cacheLifetime") now = datetime.datetime.now() seconds = (now - self.cache["timestamp"]).total_seconds() if seconds < cacheLifetime: data = self.cache["data"] api = True log.debug("Corona: returning cached API data") else: data = self.getAPI() if data: api = True self.cache["timestamp"] = datetime.datetime.now() self.cache["data"] = data log.debug("Corona: caching API data") else: now = datetime.datetime.now() midnight = now.replace(hour=0, minute=0, second=0, microsecond=0) seconds = (now - midnight).seconds if seconds > (now - self.cache["timestamp"]).total_seconds(): data = self.cache["data"] api = True log.debug( "Corona: error accessing API, returning cached API data" ) else: data = self.getCSV() if data: git = True else: data = self.getAPI() if data: api = True self.cache["timestamp"] = datetime.datetime.now() self.cache["data"] = data log.debug("Corona: caching API data") else: data = self.getCSV() if data: git = True if not data: irc.reply("Error. Unable to access database.") return total_confirmed = ( total_deaths ) = total_recovered = total_active = last_update = 0 confirmed = deaths = recovered = active = updated = 0 location = "Global" for region in data: if api: r = region.get("attributes") else: r = region if search: if api: region = r.get("Country_Region") state = r.get("Province_State") else: region = r.get("Country/Region") state = r.get("Province/State") if len(search) == 2: if self.registryValue("countryFirst", msg.channel): try: search = countries[search.upper()] except KeyError: try: search = states[search.upper()] except KeyError: pass else: try: search = states[search.upper()] except KeyError: try: search = countries[search.upper()] except KeyError: pass if search.lower() == "usa" or "united states" in search.lower(): search = "us" if "korea" in search.lower(): search = "korea, south" if region and search.lower() == region.lower(): location = region confirmed += int(r.get("Confirmed")) deaths += int(r.get("Deaths")) recovered += int(r.get("Recovered")) active += int(r.get("Active")) if api: time = int(r.get("Last_Update")) if git: time = datetime.datetime.strptime( r.get("Last Update"), "%Y-%m-%dT%H:%M:%S" ) time = int(time.timestamp() * 1000) if time > updated: updated = time local_ratio_dead = "{0:.1%}".format(deaths / confirmed) elif state and search.lower() == state.lower(): location = state confirmed += int(r.get("Confirmed")) deaths += int(r.get("Deaths")) recovered += int(r.get("Recovered")) active += int(r.get("Active")) if api: time = int(r.get("Last_Update")) if git: time = datetime.datetime.strptime( r.get("Last Update"), "%Y-%m-%dT%H:%M:%S" ) time = int(time.timestamp() * 1000) if time > updated: updated = time local_ratio_dead = "{0:.1%}".format(deaths / confirmed) total_confirmed += int(r.get("Confirmed")) total_deaths += int(r.get("Deaths")) total_recovered += int(r.get("Recovered")) total_active += int(r.get("Active")) if api: time = int(r.get("Last_Update")) if git: time = datetime.datetime.strptime( r.get("Last Update"), "%Y-%m-%dT%H:%M:%S" ) time = int(time.timestamp() * 1000) if time > last_update: last_update = time ratio_dead = "{0:.1%}".format(total_deaths / total_confirmed) template = self.registryValue("template", msg.channel) if location == "Global": last_update = self.timeCreated(last_update) template = template.replace("$location", location) template = template.replace("$confirmed", str(total_confirmed)) template = template.replace("$dead", str(total_deaths)) template = template.replace("$recovered", str(total_recovered)) template = template.replace("$ratio", ratio_dead) template = template.replace("$active", str(total_active)) template = template.replace("$updated", last_update) else: updated = self.timeCreated(updated) template = template.replace("$location", location) template = template.replace("$confirmed", str(confirmed)) template = template.replace("$dead", str(deaths)) template = template.replace("$recovered", str(recovered)) template = template.replace("$ratio", local_ratio_dead) template = template.replace("$active", str(active)) template = template.replace("$updated", updated) irc.reply(template) Class = CoronaLight