pincerna 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|