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.
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