2020-01-30 04:11:45 +01:00
# File: Wttr.pm
#
# Purpose: Weather command using Wttr.in.
2023-02-21 06:31:52 +01:00
# SPDX-FileCopyrightText: 2020-2023 Pragmatic Software <pragma78@gmail.com>
2021-07-11 00:00:22 +02:00
# SPDX-License-Identifier: MIT
2020-01-30 04:11:45 +01:00
2021-07-14 04:45:56 +02:00
package PBot::Plugin::Wttr ;
use parent 'PBot::Plugin::Base' ;
2020-01-30 04:11:45 +01:00
2021-06-19 06:23:34 +02:00
use PBot::Imports ;
2021-07-24 04:22:25 +02:00
use PBot::Core::Utils::LWPUserAgentCached ;
2021-06-19 06:23:34 +02:00
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-01-30 04:11:45 +01:00
2023-04-14 02:01:23 +02:00
sub initialize ($self, %conf) {
2021-07-31 04:01:24 +02:00
$ self - > { pbot } - > { commands } - > add (
name = > 'wttr' ,
help = > 'Provides weather information via wttr.in' ,
subref = > sub { $ self - > cmd_wttr ( @ _ ) } ,
) ;
2020-01-30 04:11:45 +01:00
}
2023-04-14 02:01:23 +02:00
sub unload ($self) {
2021-07-31 04:01:24 +02:00
$ self - > { pbot } - > { commands } - > remove ( 'wttr' ) ;
2020-01-30 04:11:45 +01:00
}
2023-04-14 02:01:23 +02:00
sub cmd_wttr ($self, $context) {
2020-05-04 22:21:35 +02:00
my $ arguments = $ context - > { arguments } ;
2020-02-15 23:38:32 +01:00
my @ wttr_options = (
2021-07-31 00:01:38 +02:00
'default' ,
'all' ,
'conditions' ,
'forecast' ,
'feelslike' ,
'uvindex' ,
'visibility' ,
'dewpoint' ,
'heatindex' ,
'cloudcover' ,
'wind' ,
'sun' ,
'sunhours' ,
'moon' ,
'chances' ,
'snowfall' ,
'location' ,
'qlocation' ,
'time' ,
'population' ,
2020-02-15 23:38:32 +01:00
) ;
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 % options ;
2021-07-31 00:01:38 +02:00
my ( $ opt_args , $ opt_error ) = $ self - > { pbot } - > { interpreter } - > getopt (
$ arguments ,
2020-02-15 23:38:32 +01:00
\ % options ,
2021-07-31 00:01:38 +02:00
[ 'bundling_override' , 'ignorecase_always' ] ,
2020-02-15 23:38:32 +01:00
'u=s' ,
'h' ,
2021-07-31 00:01:38 +02:00
@ wttr_options ,
2020-02-15 23:38:32 +01:00
) ;
2021-07-31 00:01:38 +02:00
return "/say $opt_error -- $usage" if defined $ opt_error ;
return $ usage if exists $ options { h } ;
$ 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
2021-07-09 23:39:35 +02:00
my $ userdata = $ self - > { pbot } - > { users } - > { storage } - > get_data ( $ username ) ;
2020-04-28 01:06:15 +02:00
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 } ;
}
2021-07-29 02:33:39 +02:00
my @ opts = keys % options ;
return $ self - > get_wttr ( $ arguments , \ @ opts , \ @ wttr_options ) ;
2020-01-30 04:11:45 +01:00
}
2023-04-14 02:01:23 +02:00
sub get_wttr ($self, $location, $options, $order) {
2020-02-15 23:38:32 +01:00
my % cache_opt = (
'namespace' = > 'wttr' ,
2023-01-22 08:32:14 +01:00
'default_expires_in' = > 900
2020-02-15 23:38:32 +01:00
) ;
my $ location_uri = uri_escape_utf8 $ location ;
2021-07-24 04:22:25 +02:00
my $ ua = PBot::Core::Utils::LWPUserAgentCached - > new ( \ % cache_opt , timeout = > 30 ) ;
2020-02-15 23:38:32 +01:00
my $ response = $ ua - > get ( "http://wttr.in/$location_uri?format=j1&m" ) ;
my $ json ;
2021-07-29 02:33:39 +02:00
if ( $ response - > is_success ) {
$ json = $ response - > decoded_content ;
} else {
return "Failed to fetch weather data: " . $ response - > status_line ;
}
2020-02-15 23:38:32 +01:00
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$// ;
}
2021-07-29 02:33:39 +02:00
if ( @$ options == 1 and $ options - > [ 0 ] eq 'default' ) {
2022-07-03 16:22:33 +02:00
push @$ options , ( 'chances' , 'time' ) ;
2020-04-08 08:34:37 +02:00
}
2021-07-29 02:33:39 +02:00
foreach my $ option ( @$ order ) {
next if not grep { $ _ eq $ 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)" ;
2021-07-29 02:11:44 +02:00
$ sep = ' -> ' ;
2020-02-19 00:06:07 +01:00
$ last_condition = $ condition ;
}
2020-02-15 23:38:32 +01:00
}
}
2022-05-23 02:12:57 +02:00
$ result . = "$conditions; " if length $ sep ;
$ 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 08:34:37 +02:00
2021-07-29 02:11:44 +02:00
$ result . = ";\n" ;
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'}; " ;
2021-07-29 02:11:44 +02:00
$ result . = "Humidity: $c->{'humidity'}%; Precip: $c->{'precipMM'}mm; Pressure: $c->{'pressure'}hPa; UV Index: $c->{'uvIndex'};\n" ;
2020-02-15 23:38:32 +01:00
}
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
}
}
2021-07-29 02:11:44 +02:00
$ result . = ";\n" ;
2020-02-15 23:38:32 +01:00
}
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' } ;
2021-07-29 02:11:44 +02:00
$ result =~ s/,\s+$/;\n/ ;
2020-02-15 23:38:32 +01:00
}
when ( 'wind' ) {
$ result . = "Wind: $c->{'windspeedKmph'}kph/$c->{'windspeedMiles'}mph $c->{'winddirDegree'}°/$c->{'winddir16Point'}, " ;
2021-07-29 02:11:44 +02:00
$ result . = "gust: $h->{'WindGustKmph'}kph/$h->{'WindGustMiles'}mph, chill: $h->{'WindChillC'}C/$h->{'WindChillF'}F;\n" ;
2020-02-15 23:38:32 +01:00
}
2020-02-19 02:05:08 +01:00
when ( 'qlocation' ) {
2020-02-15 23:38:32 +01:00
my $ l = $ wttr - > { 'request' } - > [ 0 ] ;
2021-07-29 02:11:44 +02:00
$ result . = "Query location: $l->{'query'} ($l->{'type'});\n" ;
2020-02-15 23:38:32 +01:00
}
2021-07-29 02:11:44 +02:00
when ( 'dewpoint' ) { $ result . = "Dew point: $h->{'DewPointC'}C/$h->{'DewPointF'}F;\n" ; }
2020-02-15 23:38:32 +01:00
2021-07-29 02:11:44 +02:00
when ( 'feelslike' ) { $ result . = "Feels like: $h->{'FeelsLikeC'}C/$h->{'FeelsLikeF'}F;\n" ; }
2020-02-15 23:38:32 +01:00
2021-07-29 02:11:44 +02:00
when ( 'heatindex' ) { $ result . = "Heat index: $h->{'HeatIndexC'}C/$h->{'HeatIndexF'}F;\n" ; }
2020-02-15 23:38:32 +01:00
when ( 'moon' ) {
my $ a = $ w - > { 'astronomy' } - > [ 0 ] ;
2021-07-29 02:11:44 +02:00
$ result . = "Moon: phase: $a->{'moon_phase'}, illumination: $a->{'moon_illumination'}%, rise: $a->{'moonrise'}, set: $a->{'moonset'};\n" ;
2020-02-15 23:38:32 +01:00
}
2020-05-20 19:34:05 +02:00
when ( 'sun' ) {
2020-02-15 23:38:32 +01:00
my $ a = $ w - > { 'astronomy' } - > [ 0 ] ;
2023-01-22 08:32:14 +01:00
$ result . = "Sun: rise: $a->{'sunrise'}, set: $a->{'sunset'}; hours: $w->{'sunHour'}; UV Index: $c->{'uvIndex'};\n" ; }
2020-02-15 23:38:32 +01:00
2021-07-29 02:11:44 +02:00
when ( 'sunhours' ) { $ result . = "Hours of sun: $w->{'sunHour'};\n" ; }
2020-02-15 23:38:32 +01:00
2021-07-29 02:11:44 +02:00
when ( 'snowfall' ) { $ result . = "Total snow: $w->{'totalSnow_cm'}cm;\n" ; }
2020-02-15 23:38:32 +01:00
2021-07-29 02:11:44 +02:00
when ( 'uvindex' ) { $ result . = "UV Index: $c->{'uvIndex'};\n" ; }
2020-02-15 23:38:32 +01:00
2021-07-29 02:11:44 +02:00
when ( 'visibility' ) { $ result . = "Visibility: $c->{'visibility'}km;\n" ; }
2020-02-15 23:38:32 +01:00
2021-07-29 02:11:44 +02:00
when ( 'cloudcover' ) { $ result . = "Cloud cover: $c->{'cloudcover'}%;\n" ; }
2020-02-15 23:38:32 +01:00
2022-07-03 16:22:33 +02:00
when ( 'time' ) { $ result . = "Observed: $c->{'localObsDateTime'};\n" ; }
2020-02-19 02:05:08 +01:00
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/, $// ;
2021-07-29 02:11:44 +02:00
$ result . = "Observation location: $location;\n" ;
2021-04-05 08:15:23 +02:00
} else {
2021-07-29 02:11:44 +02:00
$ result . = "Query location: $location;\n" ;
2021-04-05 08:15:23 +02:00
}
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 ;
2021-07-29 02:11:44 +02:00
$ result . = "Population: $population;\n" ;
2020-02-19 02:05:08 +01:00
}
2020-02-19 00:06:07 +01:00
2021-07-29 02:11:44 +02:00
default { $ result . = "Option $_ coming soon;\n" 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 ;