2020-01-30 04:11:45 +01:00
# File: Wttr.pm
# Author: pragma-
#
# Purpose: Weather command using Wttr.in.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
package Plugins::Wttr ;
2020-02-15 23:38:32 +01:00
2020-02-09 04:48:05 +01:00
use parent 'Plugins::Plugin' ;
2020-01-30 04:11:45 +01:00
2020-02-09 04:48:05 +01:00
use warnings ; use strict ;
2020-01-30 04:11:45 +01:00
use feature 'unicode_strings' ;
use utf8 ;
use feature 'switch' ;
2020-02-15 23:38:32 +01:00
2020-01-30 04:11:45 +01:00
no if $] >= 5.018 , warnings = > "experimental::smartmatch" ;
2020-01-31 06:17:58 +01:00
use PBot::Utils::LWPUserAgentCached ;
2020-01-30 04:11:45 +01:00
use JSON ;
2020-01-31 04:18:58 +01:00
use URI::Escape qw/uri_escape_utf8/ ;
2020-02-16 23:13:41 +01:00
use Getopt::Long qw( GetOptionsFromArray ) ;
2020-01-30 04:11:45 +01:00
sub initialize {
2020-02-15 23:38:32 +01:00
my ( $ self , % conf ) = @ _ ;
2020-05-04 22:21:35 +02:00
$ self - > { pbot } - > { commands } - > register ( sub { $ self - > cmd_wttr ( @ _ ) } , "wttr" , 0 ) ;
2020-01-30 04:11:45 +01:00
}
sub unload {
2020-02-15 23:38:32 +01:00
my $ self = shift ;
$ self - > { pbot } - > { commands } - > unregister ( "wttr" ) ;
2020-01-30 04:11:45 +01:00
}
2020-05-04 22:21:35 +02:00
sub cmd_wttr {
my ( $ self , $ context ) = @ _ ;
my $ arguments = $ context - > { arguments } ;
2020-02-15 23:38:32 +01:00
my @ wttr_options = (
"conditions" ,
"forecast" ,
"feelslike" ,
"uvindex" ,
"visibility" ,
"dewpoint" ,
"heatindex" ,
"cloudcover" ,
"wind" ,
2020-05-20 19:34:05 +02:00
"sun" ,
2020-02-15 23:38:32 +01:00
"moon" ,
"chances" ,
"sunhours" ,
"snowfall" ,
"location" ,
2020-02-19 02:05:08 +01:00
"qlocation" ,
2020-02-19 00:06:07 +01:00
"time" ,
2020-02-19 02:05:08 +01:00
"population" ,
2020-02-15 23:38:32 +01:00
"default" ,
"all" ,
) ;
2020-05-20 19:34:05 +02:00
my $ usage = "Usage: wttr (<location> | -u <user account>) [" . join ( ' ' , map { "-$_" } @ wttr_options ) . "]; to have me remember your location, use `my location <location>`." ;
2020-02-15 23:38:32 +01:00
my $ getopt_error ;
local $ SIG { __WARN__ } = sub {
$ getopt_error = shift ;
chomp $ getopt_error ;
} ;
Getopt::Long:: Configure ( "bundling_override" , "ignorecase_always" ) ;
my % options ;
2020-02-16 23:13:41 +01:00
my @ opt_args = $ self - > { pbot } - > { interpreter } - > split_line ( $ arguments , strip_quotes = > 1 ) ;
GetOptionsFromArray (
\ @ opt_args ,
2020-02-15 23:38:32 +01:00
\ % options ,
'u=s' ,
'h' ,
@ wttr_options
) ;
return "/say $getopt_error -- $usage" if defined $ getopt_error ;
return $ usage if exists $ options { h } ;
2020-02-16 23:13:41 +01:00
$ arguments = "@opt_args" ;
2020-02-15 23:38:32 +01:00
2020-04-28 01:06:15 +02:00
if ( defined $ options { u } ) {
my $ username = delete $ options { u } ;
2020-02-15 23:38:32 +01:00
2020-04-28 01:06:15 +02:00
my $ userdata = $ self - > { pbot } - > { users } - > { users } - > get_data ( $ username ) ;
return "No such user account $username." if not defined $ userdata ;
return "User account does not have `location` set." if not exists $ userdata - > { location } ;
$ arguments = $ userdata - > { location } ;
} else {
if ( not length $ arguments ) {
2020-05-04 22:21:35 +02:00
$ arguments = $ self - > { pbot } - > { users } - > get_user_metadata ( $ context - > { from } , $ context - > { hostmask } , 'location' ) // '' ;
2020-04-28 01:06:15 +02:00
}
}
2020-02-15 23:38:32 +01:00
if ( not length $ arguments ) { return $ usage ; }
$ options { default } = 1 if not keys % options ;
if ( defined $ options { all } ) {
% options = ( ) ;
map { my $ opt = $ _ ; $ opt =~ s/\|.*$// ; $ options { $ opt } = 1 } @ wttr_options ;
delete $ options { all } ;
delete $ options { default } ;
}
2020-04-08 08:34:37 +02:00
return $ self - > get_wttr ( $ arguments , sort keys % options ) ;
2020-01-30 04:11:45 +01:00
}
2020-01-31 04:18:58 +01:00
sub get_wttr {
2020-04-08 08:34:37 +02:00
my ( $ self , $ location , @ options ) = @ _ ;
2020-02-15 23:38:32 +01:00
my % cache_opt = (
'namespace' = > 'wttr' ,
'default_expires_in' = > 3600
) ;
my $ location_uri = uri_escape_utf8 $ location ;
my $ ua = PBot::Utils::LWPUserAgentCached - > new ( \ % cache_opt , timeout = > 30 ) ;
my $ response = $ ua - > get ( "http://wttr.in/$location_uri?format=j1&m" ) ;
my $ json ;
if ( $ response - > is_success ) { $ json = $ response - > decoded_content ; }
else { return "Failed to fetch weather data: " . $ response - > status_line ; }
2020-03-20 17:45:12 +01:00
my $ wttr = eval { decode_json $ json } ;
2020-04-28 01:06:15 +02:00
if ( $@ ) {
# error decoding json so it must not be json -- return as-is
$@ = undef ;
my $ error = $ json ;
if ( $ error =~ /^Unknown location/ ) {
$ error = "Unknown location: $location" ;
}
return $ error ;
}
2020-02-15 23:38:32 +01:00
2021-04-05 08:15:23 +02:00
# title-case location
$ location = ucfirst lc $ location ;
$ location =~ s/( |\.)(\w)/$1 . uc $2/ge ;
2020-02-19 02:05:08 +01:00
$ location =~ s/United States of America/USA/ ;
2020-02-15 23:38:32 +01:00
2020-02-20 02:05:03 +01:00
my $ result = "$location: " ;
2020-02-15 23:38:32 +01:00
my $ c = $ wttr - > { 'current_condition' } - > [ 0 ] ;
my $ w = $ wttr - > { 'weather' } - > [ 0 ] ;
my $ h = $ w - > { 'hourly' } - > [ 0 ] ;
2020-02-19 00:06:07 +01:00
my ( $ obsdate , $ obstime ) = split / / , $ c - > { 'localObsDateTime' } , 2 ;
my ( $ obshour , $ obsminute ) = split /:/ , $ obstime ;
if ( $ obsminute =~ s/ PM$// ) {
$ obshour += 12 ;
} else {
$ obsminute =~ s/ AM$// ;
}
2020-04-08 08:34:37 +02:00
if ( @ options == 1 and $ options [ 0 ] eq 'default' ) {
push @ options , 'chances' ;
}
foreach my $ option ( @ options ) {
2020-02-15 23:38:32 +01:00
given ( $ option ) {
when ( 'default' ) {
2020-04-08 09:57:56 +02:00
$ result . = "Currently: $c->{'weatherDesc'}->[0]->{'value'}: $c->{'temp_C'}C/$c->{'temp_F'}F" ;
if ( $ c - > { 'FeelsLikeC' } != $ c - > { 'temp_C' } ) {
$ result . = " (Feels like $c->{'FeelsLikeC'}C/$c->{'FeelsLikeF'}F); " ;
} else {
$ result . = '; ' ;
}
2020-02-15 23:38:32 +01:00
$ result . = "Forecast: High: $w->{maxtempC}C/$w->{maxtempF}F, Low: $w->{mintempC}C/$w->{mintempF}F; " ;
2020-04-08 09:57:56 +02:00
my $ conditions = "Condition changes: " ;
2020-02-15 23:38:32 +01:00
my $ last_condition = $ c - > { 'weatherDesc' } - > [ 0 ] - > { 'value' } ;
my $ sep = '' ;
foreach my $ hour ( @ { $ w - > { 'hourly' } } ) {
my $ condition = $ hour - > { 'weatherDesc' } - > [ 0 ] - > { 'value' } ;
my $ temp = "$hour->{FeelsLikeC}C/$hour->{FeelsLikeF}F" ;
my $ time = sprintf "%04d" , $ hour - > { 'time' } ;
$ time =~ s/(\d{2})$/:$1/ ;
if ( $ condition ne $ last_condition ) {
2020-02-19 00:06:07 +01:00
my ( $ hour , $ minute ) = split /:/ , $ time ;
if ( ( $ hour > $ obshour ) or ( $ hour == $ obshour and $ minute >= $ obsminute ) ) {
2020-04-08 09:57:56 +02:00
$ conditions . = "$sep$time: $condition ($temp)" ;
2020-02-19 00:06:07 +01:00
$ sep = '-> ' ;
$ last_condition = $ condition ;
}
2020-02-15 23:38:32 +01:00
}
}
2020-04-08 08:34:37 +02:00
if ( $ sep eq '' ) {
$ result . = "Cloud cover: $c->{'cloudcover'}%; Visibility: $c->{'visibility'}km; " ;
$ result . = "Wind: $c->{'windspeedKmph'}kph/$c->{'windspeedMiles'}mph $c->{'winddirDegree'}°/$c->{'winddir16Point'}; " ;
$ result . = "Humidity: $c->{'humidity'}%; Precip: $c->{'precipMM'}mm; Pressure: $c->{'pressure'}hPa; UV Index: $c->{'uvIndex'}" ;
2020-04-08 09:57:56 +02:00
} else {
$ result . = $ conditions ;
2020-04-08 08:34:37 +02:00
}
2020-02-19 02:05:08 +01:00
$ result . = '; ' ;
2020-02-15 23:38:32 +01:00
}
when ( 'conditions' ) {
2020-04-08 09:51:03 +02:00
$ result . = "Current conditions: $c->{'weatherDesc'}->[0]->{'value'}: $c->{'temp_C'}C/$c->{'temp_F'}F" ;
if ( $ c - > { 'FeelsLikeC' } != $ c - > { 'temp_C' } ) {
$ result . = " (Feels like $c->{'FeelsLikeC'}C/$c->{'FeelsLikeF'}F); " ;
} else {
$ result . = '; ' ;
}
2020-02-15 23:38:32 +01:00
$ result . = "Cloud cover: $c->{'cloudcover'}%; Visibility: $c->{'visibility'}km; " ;
$ result . = "Wind: $c->{'windspeedKmph'}kph/$c->{'windspeedMiles'}mph $c->{'winddirDegree'}°/$c->{'winddir16Point'}; " ;
$ result . = "Humidity: $c->{'humidity'}%; Precip: $c->{'precipMM'}mm; Pressure: $c->{'pressure'}hPa; UV Index: $c->{'uvIndex'}; " ;
}
when ( 'forecast' ) {
$ result . = "Hourly forecast: " ;
my ( $ last_temp , $ last_condition , $ sep ) = ( '' , '' , '' ) ;
foreach my $ hour ( @ { $ wttr - > { 'weather' } - > [ 0 ] - > { 'hourly' } } ) {
my $ temp = "$hour->{FeelsLikeC}C/$hour->{FeelsLikeF}F" ;
my $ condition = $ hour - > { 'weatherDesc' } - > [ 0 ] - > { 'value' } ;
my $ text = '' ;
if ( $ temp ne $ last_temp ) {
$ text . = $ temp ;
$ last_temp = $ temp ;
}
if ( $ condition ne $ last_condition ) {
$ text . = ' ' if length $ text ;
2020-02-19 00:06:07 +01:00
$ text . = "($condition)" ;
2020-02-15 23:38:32 +01:00
$ last_condition = $ condition ;
}
if ( length $ text ) {
2020-02-19 02:05:08 +01:00
my $ time = sprintf '%04d' , $ hour - > { 'time' } ;
2020-02-15 23:38:32 +01:00
$ time =~ s/(\d{2})$/:$1/ ;
2020-02-19 00:06:07 +01:00
my ( $ hour , $ minute ) = split /:/ , $ time ;
if ( ( $ hour > $ obshour ) or ( $ hour == $ obshour and $ minute >= $ obsminute ) ) {
$ result . = "$sep $time: $text" ;
$ sep = ', ' ;
}
2020-02-15 23:38:32 +01:00
}
}
$ result . = "; " ;
}
when ( 'chances' ) {
2020-02-19 02:05:08 +01:00
$ result . = 'Chances of: ' ;
$ result . = 'Fog: ' . $ h - > { 'chanceoffog' } . '%, ' if $ h - > { 'chanceoffog' } ;
$ result . = 'Frost: ' . $ h - > { 'chanceoffrost' } . '%, ' if $ h - > { 'chanceoffrost' } ;
$ result . = 'High temp: ' . $ h - > { 'chanceofhightemp' } . '%, ' if $ h - > { 'chanceofhightemp' } ;
$ result . = 'Overcast: ' . $ h - > { 'chanceofovercast' } . '%, ' if $ h - > { 'chanceofovercast' } ;
$ result . = 'Rain: ' . $ h - > { 'chanceofrain' } . '%, ' if $ h - > { 'chanceofrain' } ;
$ result . = 'Remaining dry: ' . $ h - > { 'chanceofremdry' } . '%, ' if $ h - > { 'chanceofremdry' } ;
$ result . = 'Snow: ' . $ h - > { 'chanceofsnow' } . '%, ' if $ h - > { 'chanceofsnow' } ;
$ result . = 'Sunshine: ' . $ h - > { 'chanceofsunshine' } . '%, ' if $ h - > { 'chanceofsunshine' } ;
$ result . = 'Thunder: ' . $ h - > { 'chanceofthunder' } . '%, ' if $ h - > { 'chanceofthunder' } ;
$ result . = 'Windy: ' . $ h - > { 'chanceofwindy' } . '%, ' if $ h - > { 'chanceofwindy' } ;
2020-02-15 23:38:32 +01:00
$ result =~ s/,\s+$/; / ;
}
when ( 'wind' ) {
$ result . = "Wind: $c->{'windspeedKmph'}kph/$c->{'windspeedMiles'}mph $c->{'winddirDegree'}°/$c->{'winddir16Point'}, " ;
$ result . = "gust: $h->{'WindGustKmph'}kph/$h->{'WindGustMiles'}mph, chill: $h->{'WindChillC'}C/$h->{'WindChillF'}F; " ;
}
2020-02-19 02:05:08 +01:00
when ( 'qlocation' ) {
2020-02-15 23:38:32 +01:00
my $ l = $ wttr - > { 'request' } - > [ 0 ] ;
2020-02-19 02:05:08 +01:00
$ result . = "Query location: $l->{'query'} ($l->{'type'}); " ;
2020-02-15 23:38:32 +01:00
}
when ( 'dewpoint' ) { $ result . = "Dew point: $h->{'DewPointC'}C/$h->{'DewPointF'}F; " ; }
when ( 'feelslike' ) { $ result . = "Feels like: $h->{'FeelsLikeC'}C/$h->{'FeelsLikeF'}F; " ; }
when ( 'heatindex' ) { $ result . = "Heat index: $h->{'HeatIndexC'}C/$h->{'HeatIndexF'}F; " ; }
when ( 'moon' ) {
my $ a = $ w - > { 'astronomy' } - > [ 0 ] ;
$ result . = "Moon: phase: $a->{'moon_phase'}, illumination: $a->{'moon_illumination'}%, rise: $a->{'moonrise'}, set: $a->{'moonset'}; " ;
}
2020-05-20 19:34:05 +02:00
when ( 'sun' ) {
2020-02-15 23:38:32 +01:00
my $ a = $ w - > { 'astronomy' } - > [ 0 ] ;
$ result . = "Sun: rise: $a->{'sunrise'}, set: $a->{'sunset'}; " ;
}
when ( 'sunhours' ) { $ result . = "Hours of sun: $w->{'sunHour'}; " ; }
when ( 'snowfall' ) { $ result . = "Total snow: $w->{'totalSnow_cm'}cm; " ; }
when ( 'uvindex' ) { $ result . = "UV Index: $c->{'uvIndex'}; " ; }
when ( 'visibility' ) { $ result . = "Visibility: $c->{'visibility'}km; " ; }
when ( 'cloudcover' ) { $ result . = "Cloud cover: $c->{'cloudcover'}%; " ; }
2020-02-19 02:05:08 +01:00
when ( 'time' ) { $ result . = "Observation time: $c->{'localObsDateTime'}; " ; }
when ( 'location' ) {
2021-04-05 08:15:23 +02:00
if ( exists $ wttr - > { nearest_area } ) {
my $ areaName = $ wttr - > { nearest_area } - > [ 0 ] - > { areaName } - > [ 0 ] - > { value } ;
my $ region = $ wttr - > { nearest_area } - > [ 0 ] - > { region } - > [ 0 ] - > { value } ;
my $ country = $ wttr - > { nearest_area } - > [ 0 ] - > { country } - > [ 0 ] - > { value } ;
my $ location = '' ;
$ location . = "$areaName, " if length $ areaName ;
$ location . = "$region, " if length $ region and $ region ne $ areaName ;
$ location . = "$country, " if length $ country ;
$ location =~ s/, $// ;
$ result . = "Observation location: $location" ;
} else {
$ result . = "Query location: $location" ;
}
2020-02-19 02:05:08 +01:00
}
when ( 'population' ) {
my $ population = $ wttr - > { nearest_area } - > [ 0 ] - > { population } ;
$ population =~ s/(\d)(?=(\d{3})+(\D|$))/$1\,/g ;
$ result . = "Population: $population; " ;
}
2020-02-19 00:06:07 +01:00
2020-02-15 23:38:32 +01:00
default { $ result . = "Option $_ coming soon; " unless lc $ _ eq 'u' ; }
2020-01-30 04:11:45 +01:00
}
}
2020-02-15 23:38:32 +01:00
$ result =~ s/;\s+$// ;
return $ result ;
2020-01-30 04:11:45 +01:00
}
1 ;