#!/usr/bin/env python3
# coding=utf-8
# Weather Module for Drastikbot
#
# Provides weather information from http://wttr.in
#
# Depends:
# - requests :: $ pip3 install requests
'''
Copyright (C) 2018 drastik.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
'''
import urllib.parse
import requests
from user_auth import user_auth
class Module:
def __init__(self):
self.commands = ['weather', 'weather_set', 'weather_auth']
self.manual = {
"desc": "Show weather information from http://wttr.in",
"bot_commands": {
"weather": {
"usage": lambda x: (
f"{x}weather "
),
"info": "Get weather information."
},
"weather_set": {
"usage": lambda x: (
f"{x}weather_set "
),
"info": (
"Set your default location. If a location has been"
" set, calling the weather command without arguments"
" will return the weather for that location, otherwise"
" you will be asked to provide a location. To unset"
" your location use .weather_set without any"
" arguements."
)
},
"weather_auth": {
"usage": lambda x: f"{x}weather_auth",
"info": "Toggle NickServ authentication for weather_set"
}
}
}
# Helper functions:
def unit_swap(unit):
"""Change the unit string from °C to °F or from km/h to mph and opposite"""
unit_d = {
"°C": "°F",
"°F": "°C",
"km/h": "mph",
"mph": "km/h"
}
return unit_d.get(unit)
# Temperature
def temperature_color(temperature, unit_in, unit_out):
"""Colorize and convert the temperature."""
tempcolor_d = { # celsius: color
-12: "02", -9: "12", -6: "11", 2: "10",
10: "03", 19: "09", 28: "08", 37: "07"
}
if "°C" == unit_in:
celsius = int(temperature.split("(")[0])
fahrenheit = celsius * 1.8 + 32
fahrenheit = int(round(fahrenheit, 0))
elif "°F" == unit_in:
fahrenheit = int(temperature.split("(")[0])
celsius = (fahrenheit - 32) / 1.8
celsius = int(round(celsius, 0))
else:
return "invalid input unit"
for temp, color in tempcolor_d.items():
if celsius <= temp:
if "°C" == unit_out:
return f"\x03{color} {celsius}\x0F"
elif "°F" == unit_out:
return f"\x03{color} {fahrenheit}\x0F"
# Fallback for when the temperature is too high.
if "°C" == unit_out:
return f"\x0304 {celsius}\x0F"
elif "°F" == unit_out:
return f"\x0304 {fahrenheit}\x0F"
def temp_format_range(temp_list, unit, unit_s):
ret = "Temp:"
ret += f"{temperature_color(temp_list[0], unit, unit)} -"
ret += f"{temperature_color(temp_list[1], unit, unit)} {unit} /"
ret += f"{temperature_color(temp_list[0], unit, unit_s)} -"
ret += f"{temperature_color(temp_list[1], unit, unit_s)} {unit_s}"
return ret
def temp_format(txt):
temperature_list = txt.split() # ['25..27', '°C']
temperature = temperature_list[0]
# dashes = temperature.count('-')
unit = temperature_list[1]
unit_s = unit_swap(unit)
ret = "Temp:"
if '..' in temperature:
temp_list = temperature.split('..')
ret = temp_format_range(temp_list, unit, unit_s)
else:
ret += f"{temperature_color(temperature, unit, unit)} {unit} /"
ret += f"{temperature_color(temperature, unit, unit_s)} {unit_s}"
return ret
# Wind
def wind_color(wind, unit_in, unit_out):
windcolor_d = { # km/h: color
4: "03", 10: "09", 20: "08", 32: "07"
}
if "km/h" == unit_in:
kmh = int(wind)
mph = kmh * 0.6213711922
mph = int(round(mph, 0))
elif "mph" == unit_in:
mph = int(wind)
kmh = mph * 1.609344
kmh = int(round(kmh, 0))
# elif "m/s" == unit_in:
# ms = int(wind)
# kmh = ms * 3.6
for k, color in windcolor_d.items():
if kmh < k:
if "km/h" == unit_out:
return f"\x03{color} {kmh}\x0F"
elif "mph" == unit_out:
return f"\x03{color} {mph}\x0F"
# Fallback for when the wind speed is too high.
if "km/h" == unit_out:
return f"\x0304 {kmh}\x0F"
elif "mph" == unit_out:
return f"\x0304 {mph}\x0F"
def wind_format(txt):
def range_hdl(t, tempstr):
for idx, i in enumerate(t):
coltemp = wind_color(i, unit)
tempstr += f'{coltemp}'
if idx == 0:
tempstr += ' -'
return tempstr
wind_list = txt.split() # ['↑', '23', 'km/h']
icon = wind_list[0]
wind = wind_list[1]
unit = wind_list[2]
unit_s = unit_swap(unit)
dashes = wind.count('-')
ret = f'Wind: {icon}'
if 1 == dashes:
# NEEDLESS?
wind_list = wind.split('-')
ret += f"{wind_color(wind_list[0], unit, unit)} -"
ret += f"{wind_color(wind_list[1], unit, unit)} {unit} /"
ret += f"{wind_color(wind_list[0], unit, unit_s)} -"
ret += f"{wind_color(wind_list[1], unit, unit_s)} {unit_s}"
else: # 17 km/h
ret += f"{wind_color(wind, unit, unit)} {unit} /"
ret += f"{wind_color(wind, unit, unit_s)} {unit_s}"
return f'{ret}'
def handler(txt):
if ('°C' in txt) or ('°F' in txt):
return temp_format(txt)
elif ('km/h' in txt) or ('mph' in txt) or ('m/s' in txt):
return wind_format(txt)
elif ('km' in txt) or ('mi' in txt):
return f'Visibility: {txt}'
elif ('mm' in txt) or ('in' in txt):
return f'Rainfall: {txt}'
elif '%' in txt:
return f'Rain Prob: {txt}'
else:
return txt
# Ascii art set from:
# https://github.com/schachmat/wego/blob/master/frontends/ascii-art-table.go
art = (
' ',
' .-. ',
' __) ',
' ( ',
' `-᾿ ',
' • ',
' .--. ',
' .-( ). ',
' (___.__)__) ',
' _ - _ - _ ',
' _ - _ - _ - ',
' ( ). ',
' (___(__) ',
' ‚ʻ‚ʻ‚ʻ‚ʻ ',
' _`/"".-. ',
' ,\\_( ). ',
' /(___(__) ',
' ‚ʻ‚ʻ‚ʻ‚ʻ ',
' ‚’‚’‚’‚’ ',
' ‚‘‚‘‚‘‚‘ ',
' .-. ',
' * * * * ',
' * * * * ',
' * * * * ',
' ʻ ʻ ʻ ʻ ',
' ʻ ʻ ʻ ʻ ',
' ʻ ʻ ʻ ʻ ',
' ‘ ‘ ‘ ‘ ',
' ʻ * ʻ * ',
' * ʻ * ʻ ',
' ʻ * ʻ * ',
' * ʻ * ʻ ',
' * * * ',
' * * * ',
' * * * ',
' \\ / ',
' _ /"".-. ',
' \\_( ). ',
' \\ / ',
' ‒ ( ) ‒ ',
' / \\ ',
' ‚ʻ⚡ʻ‚⚡‚ʻ ',
' ‚ʻ‚ʻ⚡ʻ‚ʻ ',
' ⚡ʻ ʻ⚡ʻ ʻ ',
' *⚡ *⚡ * ',
' ― ( ) ― ',
' `-’ ',
' ⚡‘‘⚡‘‘ '
)
def wttr(irc, channel, location):
if location.lower() == 'moon' or 'moon@' in location.lower():
irc.privmsg(channel, 'This is not supported yet '
'(add ,+US or ,+France for these cities)')
return
location = urllib.parse.quote_plus(location)
url = f'http://wttr.in/{location}?0Tm'
r = requests.get(url, timeout=10).text.splitlines()
text = ''
for line in r:
for i in art:
line = line.replace(i, '')
if line:
line = handler(line)
text += f'{line} | '
text = " ".join(text.split()) # Remove additional spaces.
text = text.lstrip("Rainfall: ") # Remove 'Rainfall: ' from the front.
if "ERROR: Unknown location:" in text:
text = f'\x0304wttr.in: Location "{location}" could not be found.'
elif "API key has reached calls per day allowed limit." in text\
or ("Sorry, we are running out of queries to the weather service at "
"the moment.") in text:
text = "\x0304wttr.in: API call limit reached. Try again tomorrow."
irc.privmsg(channel, text)
# Authentication
def set_auth(i, irc, dbc):
if not user_auth(i, irc, i.nickname):
return f"{i.nickname}: You are not logged in with NickServ."
dbc.execute('SELECT auth FROM weather WHERE nickname=?;',
(i.nickname,))
fetch = dbc.fetchone()
try:
auth = fetch[0]
except TypeError: # 'NoneType' object is not subscriptable
auth = 0
if auth == 0:
auth = 1
msg = f'{i.nickname}: weather: Enabled NickServ authentication.'
elif auth == 1:
auth = 0
msg = f'{i.nickname}: weather: Disabled NickServ authentication.'
dbc.execute(
"INSERT OR IGNORE INTO weather (nickname, auth) VALUES (?, ?);",
(i.nickname, auth))
dbc.execute("UPDATE weather SET auth=? WHERE nickname=?;",
(auth, i.nickname))
return msg
def get_auth(i, irc, dbc):
dbc.execute('SELECT auth FROM weather WHERE nickname=?;', (i.nickname,))
fetch = dbc.fetchone()
try:
auth = fetch[0]
except TypeError: # 'NoneType' object is not subscriptable
auth = 0
if auth == 0:
return True
elif auth == 1 and user_auth(i, irc, i.nickname):
return True
else:
return False
def set_location(i, irc, dbc, location):
if not get_auth(i, irc, dbc):
return f"{i.nickname}: weather: NickServ authentication is required."
dbc.execute(
'''INSERT OR IGNORE INTO weather (nickname, location)
VALUES (?, ?);''', (i.nickname, location))
dbc.execute('''UPDATE weather SET location=? WHERE nickname=?;''',
(location, i.nickname))
return f'{i.nickname}: weather: Your location was set to "{location}"'
def get_location(dbc, nickname):
try:
dbc.execute(
'SELECT location FROM weather WHERE nickname=?;', (nickname,))
return dbc.fetchone()[0]
except Exception:
return False
def main(i, irc):
dbc = i.db[1].cursor()
try:
dbc.execute(
'''CREATE TABLE IF NOT EXISTS weather (nickname TEXT COLLATE NOCASE
PRIMARY KEY, location TEXT, auth INTEGER DEFAULT 0);''')
except Exception:
# sqlite3.OperationalError: cannot commit - no transaction is active
pass
if "weather" == i.cmd:
if not i.msg_nocmd:
location = get_location(dbc, i.nickname)
if location:
wttr(irc, i.channel, location)
else:
msg = (f'Usage: {i.cmd_prefix}{i.cmd} '
'')
irc.privmsg(i.channel, msg)
else:
wttr(irc, i.channel, i.msg_nocmd)
elif "weather_set" == i.cmd:
ret = set_location(i, irc, dbc, i.msg_nocmd)
i.db[1].commit()
irc.privmsg(i.channel, ret)
elif "weather_auth" == i.cmd:
ret = set_auth(i, irc, dbc)
i.db[1].commit()
irc.privmsg(i.channel, ret)