#!/usr/bin/env python
# Originally authored by Stephen Sekula (https://hub.polari.us/steve)
# Distributed under an Apache 2.0 license
# For more information ,see
import re
import pycurl
import cStringIO
import json
import sys
import time
import subprocess
import os
import unicodedata
from datetime import datetime
from pypump import PyPump, Client
from pypump.models.image import Image
from pypump.models.collection import Collection
from pypump.models.collection import Public
from pypump.models.person import Person
from pypump.exception import PyPumpException
# city,region search pattern
citypattern = re.compile('.*?([A-Za-z]+,.*)',re.DOTALL)
tripletpattern = re.compile('.*?([A-Za-z\-\s]+,\s{0,1}[A-Za-z]+,\s{0,1}[A-Za-z]+).*',re.DOTALL)
doubletpattern = re.compile('.*?([A-Za-z\-\s]+,\s{0,1}[A-Za-z]+).*',re.DOTALL)
for_tripletpattern = re.compile('^.*?\sfor %s' % (tripletpattern.pattern),re.DOTALL)
in_tripletpattern = re.compile('^.*?\sin %s' % (tripletpattern.pattern),re.DOTALL)
for_doubletpattern = re.compile('^.*?\sfor %s' % (doubletpattern.pattern),re.DOTALL)
in_doubletpattern = re.compile('^.*?\sin %s' % (doubletpattern.pattern),re.DOTALL)
# Place the credentials below for the pump.io account
# These can be obtained by using: http://polari.us/dokuwiki/doku.php?id=navierstokes#a_simple_program_to_authenticate_pypump_against_your_pumpio_instance
client_credentials = ['XXX', 'XXX']
client_tokens = ['XXX', 'XXX']
# Webfinger for your account
webfinger = "name@example.com"
# Important: define a log file that contains a list of activities to which
# we have already responded.
logfile_name = "weatherbot.activity.log"
def simple_verifier(url):
print 'Go to: ' + url
return raw_input('Verifier: ') # they will get a code back
def HTMLToText( html_text ):
pid = os.getpid()
htmlfile = open('/tmp/%d_msg.html' % (pid),'w')
try:
htmlfile.write( html_text )
except UnicodeEncodeError:
htmlfile.write( unicodedata.normalize('NFKD', html_text).encode('ascii','ignore') )
pass
htmlfile.close()
try:
ascii_text = subprocess.check_output(["lynx", "--dump", "-width 2048", "-nolist", "/tmp/%d_msg.html" % (pid)])
except subprocess.CalledProcessError:
print "There was a problem trying to call lynx - make sure it is installed correctly."
return html_text
return ascii_text
def reply(activity,my_reply):
try:
activity.obj.comment(my_reply)
except PyPumpException:
print " ... PyPumpException - ERROR - I will try to get this request next time!"
return False
pass
return True
def KelvinToCelsius(temp_kelvin):
return temp_kelvin - 273.15
def CelsiusToFarenheit(temp_celsius):
return temp_celsius*1.8 + 32.0
def FarenheitToCelsius(temp_farenheit):
return (temp_farenheit - 32.0)/1.8
def KPHToMPH(speed_kph):
return speed_kph/0.6214
def current_conditions(weather_data):
humidity = weather_data['main']['humidity']
temp_kelvin = weather_data['main']['temp']
temp_celsius = KelvinToCelsius(temp_kelvin)
temp_farenheit = CelsiusToFarenheit(temp_celsius)
wind_speed_kmh = weather_data['wind']['speed']
wind_speed_mph = KPHToMPH(wind_speed_kmh)
heat_index_farenheit = -42.379 + 2.04901523*temp_farenheit + 10.14333127*humidity - .22475541*temp_farenheit*humidity - .00683783*temp_farenheit*temp_farenheit - .05481717*humidity*humidity + .00122874*temp_farenheit*temp_farenheit*humidity + .00085282*temp_farenheit*humidity*humidity - .00000199*temp_farenheit*temp_farenheit*humidity*humidity
heat_index_celsius = FarenheitToCelsius(heat_index_farenheit)
response = ""
response += "The weather in %s is currently:
\n" % weather_data["name"]
response += "" % (weather_data['weather'][0]['icon'])
response += "\n"
response += "- Temperature: %0.1fC (%0.1fF)
\n" % (temp_celsius, temp_farenheit)
response += "- Humidity: %0.1f%%
\n" % (humidity)
response += "- Heat Index: %0.1fC (%0.1fF)
\n" % (heat_index_celsius,heat_index_farenheit)
response += "- Wind Speed: %.1fkm/h (%.1fmph)
\n" % (wind_speed_kmh,wind_speed_mph)
response += "- Conditions: %s - %s
\n" % (weather_data['weather'][0]['main'], weather_data['weather'][0]['description'])
response += "
\n"
return response
def forecast(weather_data):
list_of_data = weather_data['list']
response = ""
response += "The forecast for %s is:
\n" % weather_data["city"]["name"]
response += "\n"
response += "TIME Temp Humidity Description \n"
for moment in list_of_data:
dt = datetime.fromtimestamp(moment["dt"])
timestamp = dt.strftime('%Y-%m-%d %H:%M:%S')
temp_celsius = KelvinToCelsius(moment["main"]["temp"])
temp_farenheit = CelsiusToFarenheit(temp_celsius)
humidity = moment["main"]["humidity"]
description = "%s - %s" % (moment["weather"][0]["main"], moment["weather"][0]["description"])
icon = "http://openweathermap.org/img/w/%s.png" % (moment["weather"][0]["icon"])
response += "%s %s C (%s F) %s%% %s " % (timestamp,temp_celsius,temp_farenheit,humidity,icon,description)
response += "
"
return response
client = Client(
webfinger,
name="PyPump",
type="native",
key=client_credentials[0], # client key
secret=client_credentials[1] # client secret
)
# archive of activities I have already processed
process_log = open(logfile_name,'a+')
while 1==1:
pump = PyPump(
client=client,
token=client_tokens[0], # the token key
secret=client_tokens[1], # the token secret
verifier_callback=simple_verifier
)
my_inbox = pump.me.inbox
for activity in my_inbox.major[:100]:
author = None
to = None
cc = None
content = None
id = None
try:
author = activity.obj.author
to = getattr(activity, "to", [])
cc = getattr(activity, "cc", [])
content = activity.obj.content
id = activity.obj.id
except AttributeError:
continue
if content == None:
continue
for recipient in to:
if isinstance(recipient, Person):
if recipient.webfinger == pump.client.webfinger:
# check to see if we already handled this request
do_i_respond = True
process_log.seek(0)
for processed_id in process_log:
processed_id = processed_id.rstrip()
if activity.obj.id == processed_id:
do_i_respond = False
pass
pass
if not do_i_respond:
continue
# handle this content
text_content = HTMLToText(content)
text_content = text_content.lower()
query = -1
if text_content.find('current') != -1 or \
text_content.find('conditions') != -1 or \
text_content.find('currently') != -1:
query = 0
elif text_content.find('forecast') != -1:
query = 1
pass
else:
# default to current conditions
query = 0
search_result = in_tripletpattern.match(text_content)
if not search_result:
print " ... Search failed for in + triplet"
search_result = for_tripletpattern.match(text_content)
if not search_result:
print " ... Search failed for for + triplet"
search_result = tripletpattern.match(text_content)
if not search_result:
print " ... Search failed for pure triplet"
search_result = in_doubletpattern.match(text_content)
if not search_result:
print " ... Search failed for in + doublet"
search_result = for_doubletpattern.match(text_content)
if not search_result:
print " ... Search failed for for + doublet"
search_result = doubletpattern.match(text_content)
if not search_result:
print " ... Search failed for pure doublet"
pass
if query >= 0 and search_result:
cityrequest = search_result.group(1)
query_type = "current conditions"
if query == 1:
query_type = "forecast"
pass
author = ""
if type(activity.obj.author) == Person:
author = activity.obj.author.display_name
elif type(activity.obj.author) == unicode:
author = unicodedata.normalize('NFKD', activity.obj.author).encode('ascii','ignore')
else:
author = activity.obj.author
pass
print "%s sent a weather request for the %s for %s, which I will now process..." % (author, query_type, cityrequest)
# clean up poor formatting in content
cityrequest = cityrequest.replace(", ", ",")
# send city information to openweathermap.org
curl_tries = 0
curl_success = False
curl_buffer = cStringIO.StringIO()
while curl_success == False and curl_tries < 5:
c = pycurl.Curl()
if query == 0:
# current conditions
c.setopt(c.URL, 'http://api.openweathermap.org/data/2.5/weather?q=%s' % (cityrequest))
elif query == 1:
# forecast
c.setopt(c.URL, 'http://api.openweathermap.org/data/2.5/forecast?q=%s' % (cityrequest))
pass
c.setopt(c.WRITEFUNCTION, curl_buffer.write)
try:
c.perform()
except pycurl.error:
curl_tries += 1
time.sleep(10)
pass
curl_success = True
c.close()
if curl_success == False:
response = "There was a problem getting the response from OpenWeatherMaps.org. It could be them; or, it could be that you did not give me a valid \"City,Region\". Please try again.
"
my_reply = pump.Comment(response)
success = reply(activity,my_reply)
if success:
process_log.write(activity.obj.id+"\n")
print " ... user did not send a valid City,Region pair or there is a problem with OpenWeatherMaps.org."
else:
continue
pass
# Try loading the data from the JSON
try:
weather_data = json.loads(curl_buffer.getvalue())
curl_buffer.close()
except ValueError:
response = "There was a problem getting the response from OpenWeatherMaps.org. It could be them; or, it could be that you did not give me a valid \"City,Region\" pair from you. Please try again.
"
my_reply = pump.Comment(response)
success = reply(activity,my_reply)
curl_buffer.close()
process_log.write(activity.obj.id+"\n")
print " ... your request resulted in a bad response by the OpenWeatherMap.org API. Check it for funny business. Sorry!"
continue
try:
if query == 0:
response = current_conditions(weather_data)
elif query == 1:
response = forecast(weather_data)
pass
my_reply = pump.Comment(response)
my_reply.cc = cc
my_reply.cc.append( pump.Public )
try:
activity.obj.comment(my_reply)
process_log.write(activity.obj.id+"\n")
print " ... successfully handled the request and responded"
except PyPumpException:
print " ... PyPumpException - ERROR - I will try to get this request next time!"
continue
except KeyError:
response = "There was a problem getting the response from OpenWeatherMaps.org. It could be them; or, it could be that you did not give me a valid \"City,Region\" string. Please try again.
"
my_reply = pump.Comment(response)
success = reply(activity,my_reply)
curl_buffer.close()
process_log.write(activity.obj.id+"\n")
print " ... got bad data from the API, it seems. Letting the user know."
continue
pass
pass
pass
pass
pass
pass
print "Sleeping until next cycle..."
time.sleep(30)
process_log.close()
===== Talking to WeatherBot =====
WeatherBot (e.g. [[https://hub.polari.us/weather]]) can now take various request forms. Sending these:
New York,NY,US
or
Stockholm,SE
results in the original behavior - you get the current conditions.
Sending any of these:
Current conditions in Jefferson City,MO,US
Currently Uppsala,SE
Current in London,UK
Conditions for Moscow,RU
etc.
also results in the current conditions.
But sending any of these:
Forecast for London,UK
Forecast in Dallas,TX,US
will return a table of forecast data for the city in question, going out 3 days. This is just a first attempt to do simple request processing based on language, and, yes, it's only in English for now. But now you can at least get a forecast!
{{ :weatherbot_forecast.png?direct&300 |}}