searchlink 2.3.59

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/bin/searchlink +84 -0
  3. data/lib/searchlink/array.rb +7 -0
  4. data/lib/searchlink/config.rb +230 -0
  5. data/lib/searchlink/curl/html.rb +482 -0
  6. data/lib/searchlink/curl/json.rb +90 -0
  7. data/lib/searchlink/curl.rb +7 -0
  8. data/lib/searchlink/help.rb +103 -0
  9. data/lib/searchlink/output.rb +270 -0
  10. data/lib/searchlink/parse.rb +668 -0
  11. data/lib/searchlink/plist.rb +213 -0
  12. data/lib/searchlink/search.rb +70 -0
  13. data/lib/searchlink/searches/amazon.rb +25 -0
  14. data/lib/searchlink/searches/applemusic.rb +123 -0
  15. data/lib/searchlink/searches/bitly.rb +50 -0
  16. data/lib/searchlink/searches/definition.rb +67 -0
  17. data/lib/searchlink/searches/duckduckgo.rb +167 -0
  18. data/lib/searchlink/searches/github.rb +245 -0
  19. data/lib/searchlink/searches/google.rb +67 -0
  20. data/lib/searchlink/searches/helpers/chromium.rb +318 -0
  21. data/lib/searchlink/searches/helpers/firefox.rb +135 -0
  22. data/lib/searchlink/searches/helpers/safari.rb +133 -0
  23. data/lib/searchlink/searches/history.rb +166 -0
  24. data/lib/searchlink/searches/hook.rb +77 -0
  25. data/lib/searchlink/searches/itunes.rb +97 -0
  26. data/lib/searchlink/searches/lastfm.rb +41 -0
  27. data/lib/searchlink/searches/lyrics.rb +91 -0
  28. data/lib/searchlink/searches/pinboard.rb +183 -0
  29. data/lib/searchlink/searches/social.rb +105 -0
  30. data/lib/searchlink/searches/software.rb +27 -0
  31. data/lib/searchlink/searches/spelling.rb +59 -0
  32. data/lib/searchlink/searches/spotlight.rb +28 -0
  33. data/lib/searchlink/searches/stackoverflow.rb +31 -0
  34. data/lib/searchlink/searches/tmdb.rb +52 -0
  35. data/lib/searchlink/searches/twitter.rb +46 -0
  36. data/lib/searchlink/searches/wikipedia.rb +33 -0
  37. data/lib/searchlink/searches/youtube.rb +48 -0
  38. data/lib/searchlink/searches.rb +194 -0
  39. data/lib/searchlink/semver.rb +140 -0
  40. data/lib/searchlink/string.rb +469 -0
  41. data/lib/searchlink/url.rb +153 -0
  42. data/lib/searchlink/util.rb +87 -0
  43. data/lib/searchlink/version.rb +93 -0
  44. data/lib/searchlink/which.rb +175 -0
  45. data/lib/searchlink.rb +66 -0
  46. data/lib/tokens.rb +3 -0
  47. 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