searchlink 2.3.59
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/bin/searchlink +84 -0
- data/lib/searchlink/array.rb +7 -0
- data/lib/searchlink/config.rb +230 -0
- data/lib/searchlink/curl/html.rb +482 -0
- data/lib/searchlink/curl/json.rb +90 -0
- data/lib/searchlink/curl.rb +7 -0
- data/lib/searchlink/help.rb +103 -0
- data/lib/searchlink/output.rb +270 -0
- data/lib/searchlink/parse.rb +668 -0
- data/lib/searchlink/plist.rb +213 -0
- data/lib/searchlink/search.rb +70 -0
- data/lib/searchlink/searches/amazon.rb +25 -0
- data/lib/searchlink/searches/applemusic.rb +123 -0
- data/lib/searchlink/searches/bitly.rb +50 -0
- data/lib/searchlink/searches/definition.rb +67 -0
- data/lib/searchlink/searches/duckduckgo.rb +167 -0
- data/lib/searchlink/searches/github.rb +245 -0
- data/lib/searchlink/searches/google.rb +67 -0
- data/lib/searchlink/searches/helpers/chromium.rb +318 -0
- data/lib/searchlink/searches/helpers/firefox.rb +135 -0
- data/lib/searchlink/searches/helpers/safari.rb +133 -0
- data/lib/searchlink/searches/history.rb +166 -0
- data/lib/searchlink/searches/hook.rb +77 -0
- data/lib/searchlink/searches/itunes.rb +97 -0
- data/lib/searchlink/searches/lastfm.rb +41 -0
- data/lib/searchlink/searches/lyrics.rb +91 -0
- data/lib/searchlink/searches/pinboard.rb +183 -0
- data/lib/searchlink/searches/social.rb +105 -0
- data/lib/searchlink/searches/software.rb +27 -0
- data/lib/searchlink/searches/spelling.rb +59 -0
- data/lib/searchlink/searches/spotlight.rb +28 -0
- data/lib/searchlink/searches/stackoverflow.rb +31 -0
- data/lib/searchlink/searches/tmdb.rb +52 -0
- data/lib/searchlink/searches/twitter.rb +46 -0
- data/lib/searchlink/searches/wikipedia.rb +33 -0
- data/lib/searchlink/searches/youtube.rb +48 -0
- data/lib/searchlink/searches.rb +194 -0
- data/lib/searchlink/semver.rb +140 -0
- data/lib/searchlink/string.rb +469 -0
- data/lib/searchlink/url.rb +153 -0
- data/lib/searchlink/util.rb +87 -0
- data/lib/searchlink/version.rb +93 -0
- data/lib/searchlink/which.rb +175 -0
- data/lib/searchlink.rb +66 -0
- data/lib/tokens.rb +3 -0
- metadata +299 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
module SL
|
|
2
|
+
# DuckDuckGo Search
|
|
3
|
+
class DuckDuckGoSearch
|
|
4
|
+
class << self
|
|
5
|
+
# Returns a hash of settings for the DuckDuckGoSearch
|
|
6
|
+
# class
|
|
7
|
+
#
|
|
8
|
+
# @return [Hash] settings for the DuckDuckGoSearch
|
|
9
|
+
# class
|
|
10
|
+
#
|
|
11
|
+
def settings
|
|
12
|
+
{
|
|
13
|
+
trigger: '(?:g|ddg|z|ddgimg)',
|
|
14
|
+
searches: [
|
|
15
|
+
['g', 'Google/DuckDuckGo Search'],
|
|
16
|
+
['ddg', 'DuckDuckGo Search'],
|
|
17
|
+
['z', 'DDG Zero Click Search'],
|
|
18
|
+
['ddgimg', 'Return the first image from the destination page']
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Searches DuckDuckGo for the given search terms
|
|
24
|
+
#
|
|
25
|
+
# @param search_type [String] the type of
|
|
26
|
+
# search to perform
|
|
27
|
+
# @param search_terms [String] the terms to
|
|
28
|
+
# search for
|
|
29
|
+
# @param link_text [String] the text to
|
|
30
|
+
# display for the link
|
|
31
|
+
# @return [Array] an array containing the URL, title, and
|
|
32
|
+
# link text
|
|
33
|
+
#
|
|
34
|
+
def search(search_type, search_terms, link_text)
|
|
35
|
+
return zero_click(search_terms, link_text) if search_type =~ /^z$/
|
|
36
|
+
|
|
37
|
+
# return SL.ddg(search_terms, link_text) if search_type == 'g' && SL::GoogleSearch.test_for_key
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
terms = "%5C#{search_terms.url_encode}"
|
|
41
|
+
page = Curl::Html.new("https://duckduckgo.com/?q=#{terms}", compressed: true)
|
|
42
|
+
|
|
43
|
+
locs = page.meta['refresh'].match(%r{/l/\?uddg=(.*?)$})
|
|
44
|
+
locs = page.body.match(%r{/l/\?uddg=(.*?)'}) if locs.nil?
|
|
45
|
+
locs = page.body.match(/url=(.*?)'/) if locs.nil?
|
|
46
|
+
|
|
47
|
+
return false if locs.nil?
|
|
48
|
+
|
|
49
|
+
url = locs[1].url_decode.sub(/&rut=\w+/, '')
|
|
50
|
+
|
|
51
|
+
result = url.strip.url_decode || false
|
|
52
|
+
return false unless result
|
|
53
|
+
|
|
54
|
+
return false if result =~ /internal-search\.duckduckgo\.com/
|
|
55
|
+
|
|
56
|
+
# output_url = CGI.unescape(result)
|
|
57
|
+
output_url = result
|
|
58
|
+
|
|
59
|
+
output_title = if SL.config['include_titles'] || SL.titleize
|
|
60
|
+
SL::URL.title(output_url) || ''
|
|
61
|
+
else
|
|
62
|
+
''
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
output_url = SL.first_image(output_url) if search_type =~ /img$/
|
|
66
|
+
|
|
67
|
+
[output_url, output_title, link_text]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Searches DuckDuckGo for the given search terms and
|
|
72
|
+
# returns a zero click result
|
|
73
|
+
#
|
|
74
|
+
# @param search_terms [String] the terms to
|
|
75
|
+
# search for
|
|
76
|
+
# @param link_text [String] the text to
|
|
77
|
+
# display for the link
|
|
78
|
+
# @param disambiguate [Boolean] whether to
|
|
79
|
+
# disambiguate the search
|
|
80
|
+
#
|
|
81
|
+
# @return [Array] an array containing the URL,
|
|
82
|
+
# title, and link text
|
|
83
|
+
#
|
|
84
|
+
def zero_click(search_terms, link_text, disambiguate: false)
|
|
85
|
+
search_terms.gsub!(/%22/, '"')
|
|
86
|
+
d = disambiguate ? '0' : '1'
|
|
87
|
+
url = "http://api.duckduckgo.com/?q=#{search_terms.url_encode}&format=json&no_redirect=1&no_html=1&skip_disambig=#{d}"
|
|
88
|
+
result = Curl::Json.new(url, symbolize_names: true).json
|
|
89
|
+
return SL.ddg(terms, link_text) unless result
|
|
90
|
+
|
|
91
|
+
wiki_link = result[:AbstractURL] || result[:Redirect]
|
|
92
|
+
title = result[:Heading] || false
|
|
93
|
+
|
|
94
|
+
if !wiki_link.empty? && !title.empty?
|
|
95
|
+
[wiki_link, title, link_text]
|
|
96
|
+
elsif disambiguate
|
|
97
|
+
SL.ddg(search_terms, link_text)
|
|
98
|
+
else
|
|
99
|
+
zero_click(search_terms, link_text, disambiguate: true)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Registers the DuckDuckGoSearch class with the Searches
|
|
105
|
+
# module
|
|
106
|
+
# @param name [String] the name of the search
|
|
107
|
+
# @param type [Symbol] the type of search to
|
|
108
|
+
# perform
|
|
109
|
+
# @param klass [Class] the class to register
|
|
110
|
+
SL::Searches.register 'duckduckgo', :search, self
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# SL module methods
|
|
115
|
+
module SL
|
|
116
|
+
class << self
|
|
117
|
+
# Performs a Google search if API key is available,
|
|
118
|
+
# otherwise defaults to DuckDuckGo
|
|
119
|
+
#
|
|
120
|
+
# @param search_terms [String] The search terms
|
|
121
|
+
# @param link_text [String] The link text
|
|
122
|
+
# @param timeout [Integer] The timeout
|
|
123
|
+
#
|
|
124
|
+
def google(search_terms, link_text = nil, timeout: SL.config['timeout'], image: false)
|
|
125
|
+
if SL::GoogleSearch.test_for_key
|
|
126
|
+
s_class = 'google'
|
|
127
|
+
s_type = image ? 'img' : 'gg'
|
|
128
|
+
else
|
|
129
|
+
s_class = 'duckduckgo'
|
|
130
|
+
s_type = image ? 'ddgimg' : 'g'
|
|
131
|
+
end
|
|
132
|
+
search = proc { SL::Searches.plugins[:search][s_class][:class].search(s_type, search_terms, link_text) }
|
|
133
|
+
SL::Util.search_with_timeout(search, timeout)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Performs a DuckDuckGo search with the given search
|
|
137
|
+
# terms and link text. If link text is not provided, the
|
|
138
|
+
# first result will be returned. The search will timeout
|
|
139
|
+
# after the given number of seconds.
|
|
140
|
+
#
|
|
141
|
+
# @param search_terms [String] The search terms to
|
|
142
|
+
# use
|
|
143
|
+
# @param link_text [String] The text of the
|
|
144
|
+
# link to search for
|
|
145
|
+
# @param timeout [Integer] The timeout for
|
|
146
|
+
# the search in seconds
|
|
147
|
+
# @return [SL::Searches::Result] The search result
|
|
148
|
+
#
|
|
149
|
+
def ddg(search_terms, link_text = nil, timeout: SL.config['timeout'], google: true, image: false)
|
|
150
|
+
if google && SL::GoogleSearch.test_for_key
|
|
151
|
+
s_class = 'google'
|
|
152
|
+
s_type = image ? 'img' : 'gg'
|
|
153
|
+
else
|
|
154
|
+
s_class = 'duckduckgo'
|
|
155
|
+
s_type = image ? 'ddgimg' : 'g'
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
search = proc { SL::Searches.plugins[:search][s_class][:class].search(s_type, search_terms, link_text) }
|
|
159
|
+
SL::Util.search_with_timeout(search, timeout)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def first_image(url)
|
|
163
|
+
images = Curl::Html.new(url).images
|
|
164
|
+
images.filter { |img| img[:type] == 'img' }.first[:src]
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
module SL
|
|
2
|
+
# GitHub search
|
|
3
|
+
class GitHubSearch
|
|
4
|
+
class << self
|
|
5
|
+
def settings
|
|
6
|
+
{
|
|
7
|
+
trigger: '(?:giste?|ghu?)',
|
|
8
|
+
searches: [
|
|
9
|
+
['gh', 'GitHub User/Repo Link'],
|
|
10
|
+
['ghu', 'GitHub User Search'],
|
|
11
|
+
['gist', 'Gist Search'],
|
|
12
|
+
['giste', 'Gist Embed']
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def search(search_type, search_terms, link_text)
|
|
18
|
+
case search_type
|
|
19
|
+
when /^gist/
|
|
20
|
+
url, title, link_text = gist(search_terms, search_type, link_text)
|
|
21
|
+
when /^ghu$/
|
|
22
|
+
url, title, link_text = github_user(search_terms, link_text)
|
|
23
|
+
else
|
|
24
|
+
url, title, link_text = github(search_terms, link_text)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
link_text = title if link_text == '' || link_text == search_terms
|
|
28
|
+
|
|
29
|
+
[url, title, link_text]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def github_search_curl(endpoint, query)
|
|
33
|
+
headers = {
|
|
34
|
+
'Accept' => 'application/vnd.github+json',
|
|
35
|
+
'X-GitHub-Api-Version' => '2022-11-28',
|
|
36
|
+
}
|
|
37
|
+
headers['Authorization'] = "Bearer #{Secrets::GH_AUTH_TOKEN}" if Secrets::GH_AUTH_TOKEN
|
|
38
|
+
|
|
39
|
+
url = "https://api.github.com/search/#{endpoint}?q=#{query.url_encode}&per_page=1&page=1&order=desc"
|
|
40
|
+
res = Curl::Json.new(url, headers: headers)
|
|
41
|
+
|
|
42
|
+
if res.json.key?('total_count') && res.json['total_count'].positive?
|
|
43
|
+
res.json['items'][0]
|
|
44
|
+
else
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def user_gists(user, search_terms, page = 1)
|
|
50
|
+
headers = {
|
|
51
|
+
'Accept' => 'application/vnd.github+json',
|
|
52
|
+
'X-GitHub-Api-Version' => '2022-11-28'
|
|
53
|
+
}
|
|
54
|
+
headers['Authorization'] = "Bearer #{Secrets::GH_AUTH_TOKEN}" if Secrets::GH_AUTH_TOKEN
|
|
55
|
+
|
|
56
|
+
url = "https://api.github.com/users/#{user}/gists?per_page=100&page=#{page}"
|
|
57
|
+
|
|
58
|
+
res = Curl::Json.new(url, headers: headers).json
|
|
59
|
+
|
|
60
|
+
best = nil
|
|
61
|
+
best = filter_gists(res, search_terms) if res
|
|
62
|
+
|
|
63
|
+
if !best && res.count == 100
|
|
64
|
+
SL.notify('Paging', "Getting page #{page + 1} of #{user} gists")
|
|
65
|
+
best = user_gists(user, search_terms, page + 1)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
best
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def github(search_terms, link_text)
|
|
72
|
+
terms = search_terms.split(%r{[ /]+})
|
|
73
|
+
# SL.config['remove_seo'] = false
|
|
74
|
+
|
|
75
|
+
url = case terms.count
|
|
76
|
+
when 2
|
|
77
|
+
"https://github.com/#{terms[0]}/#{terms[1]}"
|
|
78
|
+
when 1
|
|
79
|
+
"https://github.com/#{terms[0]}"
|
|
80
|
+
else
|
|
81
|
+
nurl, title, link_text = SL.ddg("site:github.com #{search_terms}", link_text)
|
|
82
|
+
nurl
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if SL::URL.valid_link?(url)
|
|
86
|
+
title = SL::URL.title(url) if url && title.nil?
|
|
87
|
+
|
|
88
|
+
[url, title, link_text]
|
|
89
|
+
else
|
|
90
|
+
SL.notify('Searching GitHub', 'Repo not found, performing search')
|
|
91
|
+
search_github(search_terms, link_text)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def github_user(search_terms, link_text)
|
|
96
|
+
if search_terms.split(/ /).count > 1
|
|
97
|
+
query = %(#{search_terms} in:name)
|
|
98
|
+
res = github_search_curl('users', query)
|
|
99
|
+
else
|
|
100
|
+
query = %(user:#{search_terms})
|
|
101
|
+
res = github_search_curl('users', query)
|
|
102
|
+
res ||= github_search_curl('users', search_terms)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if res
|
|
106
|
+
url = res['html_url']
|
|
107
|
+
title = res['login']
|
|
108
|
+
|
|
109
|
+
[url, title, link_text]
|
|
110
|
+
else
|
|
111
|
+
[false, false, link_text]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def search_github(search_terms, link_text)
|
|
116
|
+
search_terms.gsub!(%r{(\S+)/(\S+)}, 'user:\1 \2')
|
|
117
|
+
search_terms.gsub!(/\bu\w*:(\w+)/, 'user:\1')
|
|
118
|
+
search_terms.gsub!(/\bl\w*:(\w+)/, 'language:\1')
|
|
119
|
+
search_terms.gsub!(/\bin?:r\w*/, 'in:readme')
|
|
120
|
+
search_terms.gsub!(/\bin?:t\w*/, 'in:topics')
|
|
121
|
+
search_terms.gsub!(/\bin?:d\w*/, 'in:description')
|
|
122
|
+
search_terms.gsub!(/\bin?:(t(itle)?|n(ame)?)/, 'in:name')
|
|
123
|
+
search_terms.gsub!(/\br:/, 'repo:')
|
|
124
|
+
|
|
125
|
+
search_terms += ' in:title' unless search_terms =~ /(in|user|repo):/
|
|
126
|
+
|
|
127
|
+
res = github_search_curl('repositories', search_terms)
|
|
128
|
+
|
|
129
|
+
return false unless res
|
|
130
|
+
|
|
131
|
+
url = res['html_url']
|
|
132
|
+
title = res['description'] || res['full_name']
|
|
133
|
+
[url, title, link_text]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def search_user_gists(user, search_terms)
|
|
137
|
+
best_gist = user_gists(user, search_terms, 1)
|
|
138
|
+
|
|
139
|
+
return false unless best_gist
|
|
140
|
+
|
|
141
|
+
best_gist
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def filter_gists(gists, search_terms)
|
|
145
|
+
score = 0
|
|
146
|
+
gists.map! do |g|
|
|
147
|
+
{
|
|
148
|
+
url: g['html_url'],
|
|
149
|
+
description: g['description'],
|
|
150
|
+
files: g['files'].map { |file, info| { filename: file, raw: info['raw_url'] } }
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
matches = []
|
|
154
|
+
gists.each do |g|
|
|
155
|
+
if g.key?(:files)
|
|
156
|
+
g[:files].each do |f|
|
|
157
|
+
next unless f[:filename]
|
|
158
|
+
|
|
159
|
+
score = f[:filename].matches_score(search_terms.gsub(/[^a-z0-9]/, ' '))
|
|
160
|
+
|
|
161
|
+
if score > 5
|
|
162
|
+
url = "#{g[:url]}#file-#{f[:filename].gsub(/\./, '-')}"
|
|
163
|
+
matches << { url: url, title: f[:filename], score: score }
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
score = g[:description].nil? ? 0 : g[:description].matches_score(search_terms.gsub(/[^a-z0-9]/, ' '))
|
|
169
|
+
matches << { url: g[:url], title: g[:files][0][:filename], score: score } if score > 5
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
return false if matches.empty?
|
|
173
|
+
|
|
174
|
+
matches.max_by { |m| m[:score] }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def gist(terms, type, link_text)
|
|
178
|
+
terms.strip!
|
|
179
|
+
case terms
|
|
180
|
+
# If an id (and optional file) are given, expand it to include username an generate link
|
|
181
|
+
when %r{^(?<id>[a-z0-9]{32}|[0-9]{6,10})(?:[#/](?<file>(?:file-)?.*?))?$}
|
|
182
|
+
m = Regexp.last_match
|
|
183
|
+
res = Curl::Html.new("https://gist.github.com/#{m['id']}", headers_only: true)
|
|
184
|
+
url = res.headers['location']
|
|
185
|
+
title = SL::URL.title(url)
|
|
186
|
+
|
|
187
|
+
url = "#{url}##{m['file']}" if m['file']
|
|
188
|
+
# If a user an id (an o) are given, convert to a link
|
|
189
|
+
when %r{^(?<u>\w+)/(?<id>[a-z0-9]{32}|[0-9]{6,10})(?:[#/](?<file>(?:file-)?.*?))?$}
|
|
190
|
+
m = Regexp.last_match
|
|
191
|
+
url = "https://gist.github.com/#{m['u']}/#{m['id']}"
|
|
192
|
+
title = SL::URL.title(url)
|
|
193
|
+
|
|
194
|
+
url = "#{url}##{m['file']}" if m['file']
|
|
195
|
+
# if a full gist URL is given, simply clean it up
|
|
196
|
+
when %r{(?<url>https://gist.github.com/(?:(?<user>\w+)/)?(?<id>[a-z0-9]{32}|[0-9]{6,10}))(?:[#/](?<file>(?:file-)?.*?))?$}
|
|
197
|
+
m = Regexp.last_match
|
|
198
|
+
url = m['url']
|
|
199
|
+
title = SL::URL.title(url)
|
|
200
|
+
|
|
201
|
+
url = "#{url}##{m['file']}" if m['file']
|
|
202
|
+
# Otherwise do a search of gist.github.com for the keywords
|
|
203
|
+
else
|
|
204
|
+
if terms.split(/ +/).count > 1
|
|
205
|
+
parts = terms.split(/ +/)
|
|
206
|
+
gist = search_user_gists(parts[0], parts[1..].join(' '))
|
|
207
|
+
|
|
208
|
+
if gist
|
|
209
|
+
url = gist[:url]
|
|
210
|
+
title = gist[:title]
|
|
211
|
+
else
|
|
212
|
+
url, title, link_text = SL.ddg("site:gist.github.com #{terms}", link_text)
|
|
213
|
+
end
|
|
214
|
+
else
|
|
215
|
+
url, title, link_text = SL.ddg("site:gist.github.com #{terms}", link_text)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Assuming we retrieved a full gist URL
|
|
220
|
+
if url =~ %r{https://gist.github.com/(?:(?<user>[^/]+)/)?(?<id>[a-z0-9]+?)(?:[#/](?<file>(?:file-)?.*?))?$}
|
|
221
|
+
m = Regexp.last_match
|
|
222
|
+
user = m['user']
|
|
223
|
+
id = m['id']
|
|
224
|
+
|
|
225
|
+
# If we're trying to create an embed, convert elements to a JS embed script
|
|
226
|
+
if type =~ /e$/
|
|
227
|
+
url = if m['file']
|
|
228
|
+
"https://gist.github.com/#{user}/#{id}.js?file=#{m['file'].fix_gist_file}"
|
|
229
|
+
else
|
|
230
|
+
"https://gist.github.com/#{user}/#{id}.js"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
['embed', %(<script src="#{url}"></script>), link_text]
|
|
234
|
+
else
|
|
235
|
+
[url, title, link_text]
|
|
236
|
+
end
|
|
237
|
+
else
|
|
238
|
+
[false, title, link_text]
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
SL::Searches.register 'github', :search, self
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module SL
|
|
2
|
+
# Google Search
|
|
3
|
+
class GoogleSearch
|
|
4
|
+
class << self
|
|
5
|
+
attr_reader :api_key
|
|
6
|
+
|
|
7
|
+
def settings
|
|
8
|
+
{
|
|
9
|
+
trigger: '(g(oo)?g(le?)?|img)',
|
|
10
|
+
searches: [
|
|
11
|
+
['gg', 'Google Search'],
|
|
12
|
+
['img', 'First image from result']
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_for_key
|
|
18
|
+
return false unless SL.config.key?('google_api_key') && SL.config['google_api_key']
|
|
19
|
+
|
|
20
|
+
key = SL.config['google_api_key']
|
|
21
|
+
return false if key =~ /^(x{4,})?$/i
|
|
22
|
+
|
|
23
|
+
@api_key = key
|
|
24
|
+
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def search(search_type, search_terms, link_text)
|
|
29
|
+
image = search_type =~ /img$/ ? true : false
|
|
30
|
+
|
|
31
|
+
unless test_for_key
|
|
32
|
+
SL.add_error('api key', 'Missing Google API Key')
|
|
33
|
+
return false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
url = "https://customsearch.googleapis.com/customsearch/v1?cx=338419ee5ac894523&q=#{ERB::Util.url_encode(search_terms)}&num=1&key=#{@api_key}"
|
|
37
|
+
json = Curl::Json.new(url).json
|
|
38
|
+
|
|
39
|
+
if json['error'] && json['error']['code'].to_i == 429
|
|
40
|
+
SL.notify('api limit', 'Google API limit reached, defaulting to DuckDuckGo')
|
|
41
|
+
return SL.ddg(terms, link_text, google: false, image: image)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
unless json['queries']['request'][0]['totalResults'].to_i.positive?
|
|
45
|
+
SL.notify('no results', 'Google returned no results, defaulting to DuckDuckGo')
|
|
46
|
+
return SL.ddg(terms, link_text, google: false, image: image)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
result = json['items'][0]
|
|
50
|
+
return false if result.nil?
|
|
51
|
+
|
|
52
|
+
output_url = result['link']
|
|
53
|
+
output_title = result['title']
|
|
54
|
+
output_title.remove_seo!(output_url) if SL.config['remove_seo']
|
|
55
|
+
|
|
56
|
+
output_url = SL.first_image if search_type =~ /img$/
|
|
57
|
+
|
|
58
|
+
[output_url, output_title, link_text]
|
|
59
|
+
rescue StandardError
|
|
60
|
+
SL.notify('Google error', 'Error fetching Google results, switching to DuckDuckGo')
|
|
61
|
+
SL.ddg(search_terms, link_text, google: false, image: image)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
SL::Searches.register 'google', :search, self
|
|
66
|
+
end
|
|
67
|
+
end
|