pincerna 1.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.rubocop.yml +77 -0
- data/.travis-gemfile +17 -0
- data/.travis.yml +7 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +23 -0
- data/LICENSE.md +21 -0
- data/README.md +211 -0
- data/Rakefile +45 -0
- data/bin/pincernad +56 -0
- data/docs/Pincerna.html +130 -0
- data/docs/Pincerna/Base.html +3051 -0
- data/docs/Pincerna/Bookmark.html +523 -0
- data/docs/Pincerna/Cache.html +767 -0
- data/docs/Pincerna/ChromeBookmark.html +308 -0
- data/docs/Pincerna/CurrencyConversion.html +589 -0
- data/docs/Pincerna/FirefoxBookmark.html +328 -0
- data/docs/Pincerna/Ip.html +1017 -0
- data/docs/Pincerna/Map.html +399 -0
- data/docs/Pincerna/SafariBookmark.html +308 -0
- data/docs/Pincerna/Server.html +673 -0
- data/docs/Pincerna/Translation.html +517 -0
- data/docs/Pincerna/UnitConversion.html +1042 -0
- data/docs/Pincerna/Version.html +189 -0
- data/docs/Pincerna/Vpn.html +561 -0
- data/docs/Pincerna/Weather.html +837 -0
- data/docs/_index.html +298 -0
- data/docs/class_list.html +54 -0
- data/docs/css/common.css +1 -0
- data/docs/css/full_list.css +57 -0
- data/docs/css/style.css +338 -0
- data/docs/file.README.html +327 -0
- data/docs/file_list.html +56 -0
- data/docs/frames.html +28 -0
- data/docs/index.html +327 -0
- data/docs/js/app.js +214 -0
- data/docs/js/full_list.js +178 -0
- data/docs/js/jquery.js +4 -0
- data/docs/method_list.html +389 -0
- data/docs/top-level-namespace.html +112 -0
- data/icon.png +0 -0
- data/images/chrome.png +0 -0
- data/images/currency.png +0 -0
- data/images/firefox.png +0 -0
- data/images/map.png +0 -0
- data/images/network.png +0 -0
- data/images/safari.png +0 -0
- data/images/translate.png +0 -0
- data/images/unit.png +0 -0
- data/images/vpn.png +0 -0
- data/images/weather.png +0 -0
- data/info.plist +961 -0
- data/it.cowtech.pincernad.plist +19 -0
- data/lib/pincerna.rb +30 -0
- data/lib/pincerna/base.rb +258 -0
- data/lib/pincerna/bookmark.rb +80 -0
- data/lib/pincerna/cache.rb +61 -0
- data/lib/pincerna/chrome_bookmark.rb +40 -0
- data/lib/pincerna/currency_conversion.rb +134 -0
- data/lib/pincerna/firefox_bookmark.rb +92 -0
- data/lib/pincerna/ip.rb +135 -0
- data/lib/pincerna/map.rb +30 -0
- data/lib/pincerna/safari_bookmark.rb +40 -0
- data/lib/pincerna/server.rb +85 -0
- data/lib/pincerna/translation.rb +67 -0
- data/lib/pincerna/unit_conversion.rb +120 -0
- data/lib/pincerna/version.rb +24 -0
- data/lib/pincerna/vpn.rb +74 -0
- data/lib/pincerna/weather.rb +188 -0
- data/pincerna.alfredworkflow +0 -0
- data/pincerna.gemspec +36 -0
- data/pincerna.sh +9 -0
- data/spec/cassettes/Pincerna_CurrencyConversion/_perform_filtering/should_return_valid_values.yml +38 -0
- data/spec/cassettes/Pincerna_Ip/_get_local_addresses/should_return_a_list_of_addresses.yml +47 -0
- data/spec/cassettes/Pincerna_Ip/_get_public_address/should_return_public_IP_address.yml +44 -0
- data/spec/cassettes/Pincerna_Translation/_perform_filtering/should_default_from_English_to_the_given_language_when_only_one_is_present.yml +124 -0
- data/spec/cassettes/Pincerna_Translation/_perform_filtering/should_query_Google_Translate_for_sentences_returning_no_alternatives.yml +51 -0
- data/spec/cassettes/Pincerna_Translation/_perform_filtering/should_query_Google_Translate_for_single_words.yml +71 -0
- data/spec/cassettes/Pincerna_Weather/_get_forecast/should_append_name.yml +177 -0
- data/spec/cassettes/Pincerna_Weather/_get_forecast/should_get_correct_forecasts.yml +339 -0
- data/spec/cassettes/Pincerna_Weather/_lookup_places/should_return_an_existing_WOEID_without_making_any_request.yml +56 -0
- data/spec/cassettes/Pincerna_Weather/_lookup_places/should_search_for_places.yml +189 -0
- data/spec/cassettes/Pincerna_Weather/_perform_filtering/should_get_forecast.yml +56 -0
- data/spec/coverage_helper.rb +44 -0
- data/spec/pincerna/base_spec.rb +166 -0
- data/spec/pincerna/bookmark_spec.rb +65 -0
- data/spec/pincerna/cache_spec.rb +88 -0
- data/spec/pincerna/chrome_bookmark_spec.rb +114 -0
- data/spec/pincerna/currency_conversion_spec.rb +46 -0
- data/spec/pincerna/firefox_bookmark_spec.rb +46 -0
- data/spec/pincerna/ip_spec.rb +194 -0
- data/spec/pincerna/map_spec.rb +24 -0
- data/spec/pincerna/safari_bookmark_spec.rb +237 -0
- data/spec/pincerna/server_spec.rb +108 -0
- data/spec/pincerna/translation_spec.rb +55 -0
- data/spec/pincerna/unit_conversion_spec.rb +98 -0
- data/spec/pincerna/vpn_spec.rb +68 -0
- data/spec/pincerna/weather_spec.rb +131 -0
- data/spec/spec_helper.rb +48 -0
- metadata +283 -0
@@ -0,0 +1,85 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
#
|
4
|
+
# This file is part of the pincerna gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>.
|
5
|
+
# Licensed under the MIT license, which can be found at https://choosealicense.com/licenses/mit.
|
6
|
+
#
|
7
|
+
|
8
|
+
module Pincerna
|
9
|
+
# Main HTTP server to handle requests.
|
10
|
+
class Server < Goliath::API
|
11
|
+
use Goliath::Rack::Params
|
12
|
+
use Goliath::Rack::Heartbeat
|
13
|
+
use Goliath::Rack::Validation::RequestMethod, %w(GET)
|
14
|
+
|
15
|
+
# Delay before responding to a request.
|
16
|
+
DELAY=0.05
|
17
|
+
|
18
|
+
# Enqueues a request.
|
19
|
+
def self.enqueue_request
|
20
|
+
@requests ||= Queue.new
|
21
|
+
@requests << Time.now.to_f
|
22
|
+
EM::Synchrony.sleep(DELAY)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Enqueues a request.
|
26
|
+
#
|
27
|
+
# @return [Boolean] `true` if the request was the last arrived and therefore must be performed, `false` otherwise.
|
28
|
+
def self.perform_request?
|
29
|
+
@requests.pop
|
30
|
+
@requests.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
# Handles a valid request.
|
34
|
+
#
|
35
|
+
# @param type [String] The type of request.
|
36
|
+
# @param args [Hash] The parameters of the request.
|
37
|
+
# @return [Array] A response complaint to Rack interface.
|
38
|
+
def handle_request(type, args)
|
39
|
+
# Enqueue the request. This will wait to avoid too many requests.
|
40
|
+
Server.enqueue_request
|
41
|
+
|
42
|
+
# Execute the request, if none were added.
|
43
|
+
response = Server.perform_request? ? Pincerna::Base.execute!(type, (args["q"] || "").strip, args["format"], args["debug"]) : false
|
44
|
+
|
45
|
+
if response then
|
46
|
+
[200, {"Content-Type" => response.format_content_type}, response.output]
|
47
|
+
else
|
48
|
+
[response.nil? ? 404 : 429, {"Content-Type" => "text/plain"}, ""]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Schedule the server's stop.
|
53
|
+
# @return [Array] A response complaint to Rack interface.
|
54
|
+
def handle_stop
|
55
|
+
EM.add_timer(0.1) { stop_server }
|
56
|
+
[200, {}, ""]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Send a response to a request.
|
60
|
+
#
|
61
|
+
# @param env [Goliath::Env] The environment of the request.
|
62
|
+
# @return [Array] A response complaint to Rack interface.
|
63
|
+
def response(env)
|
64
|
+
begin
|
65
|
+
type = env["REQUEST_PATH"].gsub(/\//, "")
|
66
|
+
|
67
|
+
case type
|
68
|
+
when "quit" then handle_stop
|
69
|
+
when "install" then handle_install
|
70
|
+
when "uninstall" then handle_uninstall
|
71
|
+
else handle_request(type, params)
|
72
|
+
end
|
73
|
+
rescue => e
|
74
|
+
[500, {"X-Error" => e.class.to_s, "X-Error-Message" => e.message, "Content-Type" => "text/plain"}, e.backtrace.join("\n")]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
# Stops the server.
|
80
|
+
def stop_server
|
81
|
+
Pincerna::Cache.instance.destroy
|
82
|
+
EM.stop
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# This file is part of the pincerna gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>.
|
4
|
+
# Licensed under the MIT license, which can be found at https://choosealicense.com/licenses/mit.
|
5
|
+
#
|
6
|
+
|
7
|
+
module Pincerna
|
8
|
+
# Translates text using Google Translate.
|
9
|
+
class Translation < Base
|
10
|
+
# The expression to match.
|
11
|
+
MATCHER = /^
|
12
|
+
(from\s+)?(?<from>[a-z_-]{2,5})
|
13
|
+
\s+
|
14
|
+
(to\s+)?((?<to>[a-z_-]{2,5})\s+)?
|
15
|
+
(?<text>.+)
|
16
|
+
$/mix
|
17
|
+
|
18
|
+
# Relevant groups in the match.
|
19
|
+
RELEVANT_MATCHES = {
|
20
|
+
"from" => ->(_, value) { value.downcase },
|
21
|
+
"to" => ->(_, value) { value && value.downcase },
|
22
|
+
"text" => ->(_, value) { value.strip },
|
23
|
+
}
|
24
|
+
|
25
|
+
# The icon to show for each feedback item.
|
26
|
+
ICON = Pincerna::Base::ROOT + "/images/translate.png"
|
27
|
+
|
28
|
+
# The URL of the webservice.
|
29
|
+
URL = "http://translate.google.com.br/translate_a/t"
|
30
|
+
|
31
|
+
# Translates text using Google Translate.
|
32
|
+
#
|
33
|
+
# @param from [String] The code of the source language.
|
34
|
+
# @param to [String] The code of the target language.
|
35
|
+
# @param value [String] The text to translate.
|
36
|
+
# @return [Hash|NilClass] The translation data or `nil` if the translation failed.
|
37
|
+
def perform_filtering(from, to, value)
|
38
|
+
# By default we translate from English
|
39
|
+
if !to then
|
40
|
+
to = from
|
41
|
+
from = "en"
|
42
|
+
end
|
43
|
+
|
44
|
+
response = Pincerna::Cache.instance.use("translation:#{from}:#{to}:#{value}", Pincerna::Cache::EXPIRATIONS[:short]) {
|
45
|
+
fetch_remote_resource(URL, {client: "p", text: value, sl: from, tl: to, multires: 1, ssel: 0, tsel: 0, sc: 1, ie: "UTF-8", oe: "UTF-8"})
|
46
|
+
}
|
47
|
+
|
48
|
+
# Parse results
|
49
|
+
if response["dict"] then
|
50
|
+
translations = response["dict"][0]["entry"].map {|t| t["word"] }
|
51
|
+
{main: translations.shift, alternatives: translations}
|
52
|
+
else
|
53
|
+
translation = response["sentences"][0]["trans"]
|
54
|
+
{main: translation} if translation != value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Processes items to obtain feedback items.
|
59
|
+
#
|
60
|
+
# @param results [Array] The items to process.
|
61
|
+
# @return [Array] The feedback items.
|
62
|
+
def process_results(results)
|
63
|
+
alternatives = results[:alternatives] ? "Alternatives: #{results[:alternatives].join(", ")}" : "Action this item to copy the translation on the clipboard."
|
64
|
+
[{title: results[:main], arg: results[:main], subtitle: alternatives, icon: ICON}]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# This file is part of the pincerna gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>.
|
4
|
+
# Licensed under the MIT license, which can be found at https://choosealicense.com/licenses/mit.
|
5
|
+
#
|
6
|
+
|
7
|
+
module Pincerna
|
8
|
+
# Converts a value from a unit to another.
|
9
|
+
class UnitConversion < Base
|
10
|
+
# The expression to match.
|
11
|
+
MATCHER = /^
|
12
|
+
(?<value>([+-]?)(\d+)([.,]\d+)?)
|
13
|
+
\s+
|
14
|
+
(?<from>\S+?)
|
15
|
+
\s+
|
16
|
+
(to\s+)?
|
17
|
+
(?<to>\S+)
|
18
|
+
(?<rate>\s+with\s+rate)?
|
19
|
+
(?<split>\s+split\s+units)?
|
20
|
+
$/mix
|
21
|
+
|
22
|
+
# Relevant groups in the match.
|
23
|
+
RELEVANT_MATCHES = {
|
24
|
+
"value" => ->(context, value) { context.round_float(value.gsub(",", ".").to_f) },
|
25
|
+
"from" => ->(_, value) { value },
|
26
|
+
"to" => ->(_, value) { value },
|
27
|
+
"rate" => ->(_, value) { !value.nil? }, # If show conversion rate
|
28
|
+
"split" => ->(_, value) { !value.nil? } # If group unit for ft+in and lb+oz
|
29
|
+
}
|
30
|
+
|
31
|
+
# The icon to show for each feedback item.
|
32
|
+
ICON = Pincerna::Base::ROOT + "/images/unit.png"
|
33
|
+
|
34
|
+
# Defines a new unit.
|
35
|
+
#
|
36
|
+
# @param name [String] The name of the unit.
|
37
|
+
# @param definition [String] The definition of the unit.
|
38
|
+
# @param aliases [Array] The aliases of this unit.
|
39
|
+
def self.define_unit(name, definition, aliases)
|
40
|
+
RubyUnits::Unit.define(name) do |unit|
|
41
|
+
unit.definition = RubyUnits::Unit.new(definition)
|
42
|
+
unit.aliases = aliases
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Converts a value from a unit to another.
|
47
|
+
#
|
48
|
+
# @param value [Float] The value to convert.
|
49
|
+
# @param from [String] The origin unit.
|
50
|
+
# @param to [String] The target unit.
|
51
|
+
# @param with_rate [Boolean] If to return the conversion rate in the results.
|
52
|
+
# @param multiple [Boolean] If to use multiple units for ft (ft+in) and lb/oz (lb+oz).
|
53
|
+
# @return [Hash|NilClass] The converted data or `nil` if the conversion failed.
|
54
|
+
def perform_filtering(value, from, to, with_rate, multiple)
|
55
|
+
from = check_temperature(from)
|
56
|
+
to = check_temperature(to)
|
57
|
+
converted = convert_value(value, from, to)
|
58
|
+
converted ? {from: from, to: to, value: convert_value(value, from, from), unit: convert_value(1, from, from), result: converted, rate: convert_value(1, from, to), with_rate: with_rate, multiple: multiple} : nil
|
59
|
+
end
|
60
|
+
|
61
|
+
# Processes items to obtain feedback items.
|
62
|
+
#
|
63
|
+
# @param results [Hash] The items to process.
|
64
|
+
# @return [Array] The feedback items.
|
65
|
+
def process_results(results)
|
66
|
+
multiple = results[:multiple]
|
67
|
+
title = "#{format_value(results[:value], multiple)} = #{format_value(results[:result], multiple)}"
|
68
|
+
title << " (#{format_value(results[:unit], multiple)} = #{format_value(results[:rate], multiple)})" if results[:with_rate]
|
69
|
+
|
70
|
+
[{title: title, arg: format_value(results[:result], :raw), subtitle: "Action this item to copy the converted amount on the clipboard.", icon: ICON}]
|
71
|
+
end
|
72
|
+
|
73
|
+
# Checks if a unit is a temperature and prepend "temp" if needed.
|
74
|
+
#
|
75
|
+
# @param unit [String] The unit to check.
|
76
|
+
# @return [String] The adjusted unit.
|
77
|
+
def check_temperature(unit)
|
78
|
+
unit = unit.gsub("°", "")
|
79
|
+
/^[CFKR]$/.match(unit.upcase) ? "temp#{unit.upcase}" : unit
|
80
|
+
end
|
81
|
+
|
82
|
+
# Converts a value from a unit to another.
|
83
|
+
#
|
84
|
+
# @param value [Float] The value to convert.
|
85
|
+
# @param from [String] The origin unit.
|
86
|
+
# @param to [String] The target unit.
|
87
|
+
# @return [String|NilClass] The converted unit or `nil` if the conversion failed.
|
88
|
+
def convert_value(value, from, to)
|
89
|
+
Unit.new("#{value} #{from}").convert_to(to)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Formats a value.
|
93
|
+
#
|
94
|
+
# @param value [String] The value to format.
|
95
|
+
# @param modifier [Boolean|Symbol] If to use multiple units for ft (ft+in) and lb/oz (lb+oz). If `:raw`, only the unitless (float) value is returned.
|
96
|
+
# @param precision [Fixnum] The precision to use for rounding.
|
97
|
+
# @return [String|Float] The formatted value or the unitless value.
|
98
|
+
def format_value(value, modifier = nil, precision = 3)
|
99
|
+
rounded = round_float(value.scalar.to_f, precision)
|
100
|
+
|
101
|
+
if modifier != :raw then
|
102
|
+
format = "%0.#{precision}f"
|
103
|
+
units = value.units
|
104
|
+
|
105
|
+
if modifier && units =~ /ft|oz|lb/ then
|
106
|
+
format = units == "ft" ? :ft : :lbs
|
107
|
+
elsif rounded.to_i == rounded then
|
108
|
+
format = "%0.0f"
|
109
|
+
end
|
110
|
+
|
111
|
+
value.to_s(format).gsub(", ", " ").gsub(/ temp([CFKR])/, "°\\1")
|
112
|
+
else
|
113
|
+
rounded
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
Pincerna::UnitConversion.define_unit("miles per gallon", "1 mi/gal", ["mpg" "miles-per-gallon"])
|
120
|
+
Pincerna::UnitConversion.define_unit("kilometers per liter", "1 km/L", ["kpl" "kilometers-per-liter"])
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# This file is part of the pincerna gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>.
|
4
|
+
# Licensed under the MIT license, which can be found at https://choosealicense.com/licenses/mit.
|
5
|
+
#
|
6
|
+
|
7
|
+
module Pincerna
|
8
|
+
# The current version of pincerna, according to semantic versioning.
|
9
|
+
#
|
10
|
+
# @see http://semver.org
|
11
|
+
module Version
|
12
|
+
# The major version.
|
13
|
+
MAJOR = 1
|
14
|
+
|
15
|
+
# The minor version.
|
16
|
+
MINOR = 1
|
17
|
+
|
18
|
+
# The patch version.
|
19
|
+
PATCH = 3
|
20
|
+
|
21
|
+
# The current version of pincerna.
|
22
|
+
STRING = [MAJOR, MINOR, PATCH].compact.join(".")
|
23
|
+
end
|
24
|
+
end
|
data/lib/pincerna/vpn.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# This file is part of the pincerna gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>.
|
4
|
+
# Licensed under the MIT license, which can be found at https://choosealicense.com/licenses/mit.
|
5
|
+
#
|
6
|
+
|
7
|
+
module Pincerna
|
8
|
+
# Connects or disconnects from system's VPNs.
|
9
|
+
class Vpn < Base
|
10
|
+
# The expression to match.
|
11
|
+
MATCHER = /^(?<all>.*)$/i
|
12
|
+
|
13
|
+
# The icon to show for each feedback item.
|
14
|
+
ICON = Pincerna::Base::ROOT + "/images/vpn.png"
|
15
|
+
|
16
|
+
# Connects to or disconnects from system VPN.
|
17
|
+
#
|
18
|
+
# @param query [Array] A query to match against VPNs names.
|
19
|
+
# @return [Array] A list of items to process.
|
20
|
+
def perform_filtering(query)
|
21
|
+
rv = []
|
22
|
+
interface_filter ||= query.empty? ? /.+/ : /#{query}/i
|
23
|
+
|
24
|
+
execute_command("/usr/sbin/networksetup", "-listnetworkserviceorder").split(/\n\n/).each do |i|
|
25
|
+
# Scan every interface
|
26
|
+
token = StringScanner.new(i)
|
27
|
+
|
28
|
+
if token.scan_until(/^\(\d+\)/) then
|
29
|
+
name = token.scan_until(/\n/).strip # Get VPN name
|
30
|
+
next if !interface_filter.match(name)
|
31
|
+
|
32
|
+
# Get the type
|
33
|
+
token.scan_until(/Hardware Port:\s/)
|
34
|
+
|
35
|
+
# If type matches
|
36
|
+
rv << {name: name, connected: vpn_connected?(name)} if is_vpn_service?(token.scan_until(/,/))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
rv
|
41
|
+
end
|
42
|
+
|
43
|
+
# Processes items to obtain feedback items.
|
44
|
+
#
|
45
|
+
# @param results [Array] The items to process.
|
46
|
+
# @return [Array] The feedback items.
|
47
|
+
def process_results(results)
|
48
|
+
results.map do |result|
|
49
|
+
title = "#{result[:connected] ? "Disconnect from" : "Connect to"} #{result[:name]}"
|
50
|
+
subtitle = "Action this item to #{result[:connected] ? "disconnect from" : "connect to"} the VPN service."
|
51
|
+
arg = "#{result[:connected] ? "disconnect" : "connect"} service \"#{result[:name]}\""
|
52
|
+
|
53
|
+
{title: title, arg: arg, subtitle: subtitle, icon: ICON}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Checks if a VPN is connected.
|
58
|
+
#
|
59
|
+
# @param name [String] The VPN's name.
|
60
|
+
# @return [Boolean] `true` if the VPN is connected, `false` otherwise.
|
61
|
+
def vpn_connected?(name)
|
62
|
+
execute_command("/usr/sbin/networksetup", "-showpppoestatus", "\"#{name}\"").strip == "connected"
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
# Check if a service is a VPN.
|
67
|
+
#
|
68
|
+
# @param service [String] The service name.
|
69
|
+
# @return `true` if the service is a VPN service, `false` otherwise.
|
70
|
+
def is_vpn_service?(service)
|
71
|
+
["L2TP", "IPSec"].include?(service.gsub(/,$/, ""))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# This file is part of the pincerna gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>.
|
4
|
+
# Licensed under the MIT license, which can be found at https://choosealicense.com/licenses/mit.
|
5
|
+
#
|
6
|
+
|
7
|
+
module Pincerna
|
8
|
+
# Gets weather forecast from Yahoo! Weather.
|
9
|
+
class Weather < Base
|
10
|
+
# The expression to match.
|
11
|
+
MATCHER = /^
|
12
|
+
(?<place>.+?)
|
13
|
+
(\s+in\s+(?<scale>[cf]))?
|
14
|
+
$/mix
|
15
|
+
|
16
|
+
# Relevant groups in the match.
|
17
|
+
RELEVANT_MATCHES = {
|
18
|
+
"place" => ->(_, value) { value }, # Place or WOEID
|
19
|
+
"scale" => ->(_, value) { value.nil? ? "c" : value.downcase } # Temperature scale
|
20
|
+
}
|
21
|
+
|
22
|
+
# The icon to show for each feedback item.
|
23
|
+
ICON = Pincerna::Base::ROOT + "/images/weather.png"
|
24
|
+
|
25
|
+
# The URL of the webservice.
|
26
|
+
URL = "http://where.yahooapis.com/v1/places.q(%s);count=5"
|
27
|
+
|
28
|
+
# Yahoo! API key.
|
29
|
+
API_KEY = "dj0yJmk9ZUpBZk1hQTJGRHM5JmQ9WVdrOVlUSnBjMGhUTjJVbWNHbzlOemsyTURNeU5EWXkmcz1jb25zdW1lcnNlY3JldCZ4PWRi"
|
30
|
+
|
31
|
+
# Gets forecast for a place.
|
32
|
+
#
|
33
|
+
# @param query [String] A place to search.
|
34
|
+
# @return [Array] A list of items to process.
|
35
|
+
def perform_filtering(query, scale)
|
36
|
+
places = lookup_places(query)
|
37
|
+
places.empty? ? nil : get_forecast(places, scale) if !places.empty?
|
38
|
+
end
|
39
|
+
|
40
|
+
# Processes items to obtain feedback items.
|
41
|
+
#
|
42
|
+
# @param results [Array] The items to process.
|
43
|
+
# @return [Array] The feedback items.
|
44
|
+
def process_results(results)
|
45
|
+
results.map do |result|
|
46
|
+
# Format results
|
47
|
+
current = result[:current]
|
48
|
+
forecast = result[:forecast]
|
49
|
+
combined = "#{current[:temperature]}, #{current[:description].downcase.capitalize}, wind #{current[:wind][:speed]} #{current[:wind][:direction]} - Next: #{forecast[:high]} / #{forecast[:low]}, #{forecast[:description]}"
|
50
|
+
|
51
|
+
{title: result[:name], arg: result[:link], subtitle: combined, icon: result[:image]}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Lookups a place on Yahoo! to obtain WOEID(s).
|
56
|
+
#
|
57
|
+
# @param query [String] The place to search.
|
58
|
+
# @return [Array] A list of matching places data.
|
59
|
+
def lookup_places(query)
|
60
|
+
if query !~ /^(\d+)$/ then
|
61
|
+
Pincerna::Cache.instance.use("woeid:#{query}", Pincerna::Cache::EXPIRATIONS[:long]) do
|
62
|
+
response = fetch_remote_resource(URL % CGI.escape(query), {appid: API_KEY, format: :json})
|
63
|
+
response["places"].fetch("place", []).map { |place| parse_place(place) }
|
64
|
+
end
|
65
|
+
else
|
66
|
+
# We already have the woeid. The name will be given by Yahoo!
|
67
|
+
[{woeid: query}]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Gets weather forecast for one or more places.
|
72
|
+
#
|
73
|
+
# @param places [Array] The places to query.
|
74
|
+
# @param scale [String] The unit system to use: `f` for the US system (Farenheit) and `c` for the International System one (Celsius).
|
75
|
+
# @return [Array|NilClass] An array with forecasts data or `nil` if the query failed.
|
76
|
+
def get_forecast(places, scale = "c")
|
77
|
+
client = Weatherman::Client.new(unit: scale)
|
78
|
+
temperature_unit = "°#{scale.upcase}"
|
79
|
+
|
80
|
+
places.map do |place|
|
81
|
+
Pincerna::Cache.instance.use("forecast:#{place[:woeid]}", Pincerna::Cache::EXPIRATIONS[:short]) {
|
82
|
+
parse_forecast_response(place, client.lookup_by_woeid(place[:woeid]), temperature_unit)
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Converts the degrees direction of the wind to the cardinal points notation (like NE or SW).
|
88
|
+
#
|
89
|
+
# @param degrees [Fixnum] The direction in degrees.
|
90
|
+
# @return [String] The direction in cardinal points notation.
|
91
|
+
def get_wind_direction(degrees)
|
92
|
+
# Normalize value
|
93
|
+
degrees += 360 if degrees < 0
|
94
|
+
degrees = degrees % 360
|
95
|
+
|
96
|
+
# Get the position
|
97
|
+
directions = ["N", "NE", "NE", "E", "E", "SE", "SE", "S", "S", "SW", "SW", "W", "W", "NW", "NW", "N"]
|
98
|
+
position = ((degrees.to_f / 22.5) - 0.5).ceil.to_i % directions.count # The mod operation is needed for values close to 360 who, after ceiling, would otherwise overflow.
|
99
|
+
directions[position]
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
# Gets and downloads an image for a forecast.
|
104
|
+
#
|
105
|
+
# @param url [String] The image URL.
|
106
|
+
# @return [String] The path of the downloaded image.
|
107
|
+
def download_image(url)
|
108
|
+
# Extract the URL and use it to build the path
|
109
|
+
rv = (@cache_dir + "/weather/#{File.basename(URI.parse(url).path)}")
|
110
|
+
|
111
|
+
if !File.exists?(rv) then
|
112
|
+
# Create the directory and download the file
|
113
|
+
FileUtils.mkdir_p(@cache_dir + "/weather/")
|
114
|
+
open(rv, 'wb') {|f| f.write(open(url).read) }
|
115
|
+
end
|
116
|
+
|
117
|
+
rv
|
118
|
+
end
|
119
|
+
|
120
|
+
# Gets a location name.
|
121
|
+
#
|
122
|
+
# @param location [Hash] The location data.
|
123
|
+
# @return [String] The location name.
|
124
|
+
def get_name(location)
|
125
|
+
["city", "region", "country"].map { |field| location[field].strip }.reject(&:empty?).join(", ")
|
126
|
+
end
|
127
|
+
|
128
|
+
# Parses a WOEID lookup.
|
129
|
+
#
|
130
|
+
# @param place [Hash] The place to parse.
|
131
|
+
# @return [Hash] The parsed place.
|
132
|
+
def parse_place(place)
|
133
|
+
{
|
134
|
+
woeid: place["woeid"],
|
135
|
+
name: ["locality1", "admin3", "admin2", "admin1", "country"].map { |field| place[field] }.reject(&:empty?).uniq.join(", ")
|
136
|
+
}
|
137
|
+
end
|
138
|
+
|
139
|
+
# Formats a weather forecast.
|
140
|
+
#
|
141
|
+
# @param place [Hash] The basic place information.
|
142
|
+
# @param response [Weatherman::Response] The forecast response.
|
143
|
+
# @param temperature_unit [String] The temperature unit.
|
144
|
+
# @return [Hash] A formatted forecast.
|
145
|
+
def parse_forecast_response(place, response, temperature_unit)
|
146
|
+
image, link = extract_forecast_media(response)
|
147
|
+
place[:name] ||= get_name(response.location)
|
148
|
+
|
149
|
+
format_forecast(place, download_image(image), link, response.condition, response.forecasts.first, response.wind, temperature_unit, response.units["speed"])
|
150
|
+
end
|
151
|
+
|
152
|
+
# Formats a weather forecast.
|
153
|
+
#
|
154
|
+
# @param place [Hash] The basic place information.
|
155
|
+
# @param image [String] The icon for the current weather conditions.
|
156
|
+
# @param link [String] The link to view weather conditions on Yahoo!.
|
157
|
+
# @param current [Hash] The current weather conditions.
|
158
|
+
# @param forecast [Hash] The weather forecast for tomorrow.
|
159
|
+
# @param wind [Hash] The current wind conditions.
|
160
|
+
# @param temperature_unit [String] The temperature unit.
|
161
|
+
# @param speed_unit [String] The speed unit.
|
162
|
+
# @return [Hash] The parsed forecast.
|
163
|
+
def format_forecast(place, image, link, current, forecast, wind, temperature_unit, speed_unit)
|
164
|
+
place.merge({
|
165
|
+
image: image,
|
166
|
+
link: link,
|
167
|
+
current: {
|
168
|
+
description: current["text"],
|
169
|
+
temperature: "#{current["temp"]} #{temperature_unit}",
|
170
|
+
wind: {speed: "#{wind["speed"]} #{speed_unit}", direction: get_wind_direction(wind["direction"])}
|
171
|
+
},
|
172
|
+
forecast: {
|
173
|
+
description: forecast["text"],
|
174
|
+
high: "#{forecast["high"]} #{temperature_unit}",
|
175
|
+
low: "#{forecast["low"]} #{temperature_unit}"
|
176
|
+
},
|
177
|
+
})
|
178
|
+
end
|
179
|
+
|
180
|
+
# Extracts forecast media from a response.
|
181
|
+
#
|
182
|
+
# @param response The response to analyze.
|
183
|
+
# @return [Array] An array of media.
|
184
|
+
def extract_forecast_media(response)
|
185
|
+
[response.description_image.attr("src"), response.document_root.at_xpath("link").content.to_s]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|