stackoverflow 0.1.0
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.
- data/lib/stackoverflow.rb +178 -0
- metadata +64 -0
@@ -0,0 +1,178 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'open-uri'
|
5
|
+
require 'net/http'
|
6
|
+
require 'json'
|
7
|
+
require 'nokogiri'
|
8
|
+
require 'terminal-table/import'
|
9
|
+
require 'sqlite3'
|
10
|
+
require 'optparse'
|
11
|
+
|
12
|
+
|
13
|
+
module StackOverflow
|
14
|
+
class API
|
15
|
+
def search(search_string)
|
16
|
+
search_string = URI::encode(search_string)
|
17
|
+
api_get("/2.0/similar?order=desc&sort=votes&title=#{search_string}&site=stackoverflow&filter=!9Tk5iz1Gf")
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_question(question_id)
|
21
|
+
api_get("/2.0/questions/#{question_id}?order=desc&sort=activity&site=stackoverflow&filter=!9Tk5izFWA")
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_answers(question_id)
|
25
|
+
api_get("/2.0/questions/#{question_id}/answers?order=desc&sort=votes&site=stackoverflow&filter=!9Tk6JYC_e")
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def api_get(path)
|
31
|
+
url = "https://api.stackexchange.com" + path
|
32
|
+
u = URI.parse(url)
|
33
|
+
Net::HTTP.start(u.host, u.port, :use_ssl => true) do |http|
|
34
|
+
response = http.get(u.request_uri)
|
35
|
+
return JSON(response.body)['items']
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class DB
|
41
|
+
def initialize
|
42
|
+
@db = SQLite3::Database.new("../data/stackoverflow.db")
|
43
|
+
end
|
44
|
+
|
45
|
+
def search(search_string)
|
46
|
+
sql = "SELECT id, score, body, title FROM posts WHERE post_type_id=1 AND "
|
47
|
+
sub_sql = []
|
48
|
+
for search_term in search_string.split(' ')
|
49
|
+
sub_sql << "title LIKE '%#{search_term}%"
|
50
|
+
end
|
51
|
+
sql += sub_sql.join(' AND ')
|
52
|
+
sql += ' ORDER BY score DESC'
|
53
|
+
|
54
|
+
questions = []
|
55
|
+
@db.execute(sql) do |row|
|
56
|
+
questions << { :id => row[0],
|
57
|
+
:score => row[1],
|
58
|
+
:body => row[2],
|
59
|
+
:title => row[3],
|
60
|
+
:link => '',
|
61
|
+
:answers => get_answers(row[0]) }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_answers(question_id)
|
66
|
+
answers = []
|
67
|
+
sql = "SELECT id, score, body FROM posts WHERE post_type_id=2 AND parent_id=#{question_id} ORDER BY score DESC"
|
68
|
+
@db.execute(sql) do |row|
|
69
|
+
answers << { :id => row[0],
|
70
|
+
:score => row[1],
|
71
|
+
:body => row[2] }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class Formatter
|
77
|
+
def questions_list(questions)
|
78
|
+
nb = 1
|
79
|
+
|
80
|
+
table = Terminal::Table.new do |t|
|
81
|
+
questions.each do |question|
|
82
|
+
score = question['score'] > 0 ? "+#{question['score']}" : question['score']
|
83
|
+
t << ["[#{nb}]", "(#{score})", question['title']]
|
84
|
+
nb += 1
|
85
|
+
end
|
86
|
+
t.style = {:padding_left => 2, :border_x => " ", :border_i => " ", :border_y => " "}
|
87
|
+
end
|
88
|
+
puts table
|
89
|
+
end
|
90
|
+
|
91
|
+
def question_viewer(question)
|
92
|
+
answers = question['answers']
|
93
|
+
nb = 1
|
94
|
+
|
95
|
+
table = Terminal::Table.new do |t|
|
96
|
+
t << ["Question", html2text(question['body'])]
|
97
|
+
t.title = question['title'] + "\n\n" + question['link']
|
98
|
+
t << :separator
|
99
|
+
t << :separator
|
100
|
+
|
101
|
+
answers.each do |answer|
|
102
|
+
text = html2text(answer['body'])
|
103
|
+
t << ["[#{nb}] (+#{answer['score']})", "#{text}"]
|
104
|
+
t << :separator
|
105
|
+
nb += 1
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
IO.popen("less", "w") { |f| f.puts table }
|
110
|
+
end
|
111
|
+
|
112
|
+
def html2text(html)
|
113
|
+
doc = Nokogiri::HTML(html)
|
114
|
+
doc = doc.css('body').text.squeeze(" ").squeeze("\n").gsub(/[\n]+/, "\n\n")
|
115
|
+
wordwrap(doc)
|
116
|
+
end
|
117
|
+
|
118
|
+
def wordwrap(str, columns=80)
|
119
|
+
str.gsub(/\t/, " ").gsub(/.{1,#{ columns }}(?:\s|\Z)/) do
|
120
|
+
($& + 5.chr).gsub(/\n\005/, "\n").gsub(/\005/, "\n")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class Command
|
126
|
+
def run
|
127
|
+
options = {}
|
128
|
+
OptionParser.new do |opts|
|
129
|
+
opts.banner = "Usage: so [options] <search string> [<question_id>]"
|
130
|
+
opts.on("-h", "--help", "Help") do |v|
|
131
|
+
help(opts)
|
132
|
+
end
|
133
|
+
opts.on("-o", "--offline", "Offline mode") do |v|
|
134
|
+
options['offline'] = true
|
135
|
+
end
|
136
|
+
end.parse!
|
137
|
+
|
138
|
+
# last argument is integer when user is specifing a question_nb from the results
|
139
|
+
question_nb = nil
|
140
|
+
if ARGV[-1] =~ /^[0-9]+$/
|
141
|
+
question_nb = ARGV.pop.to_i
|
142
|
+
end
|
143
|
+
|
144
|
+
search_string = ARGV.join(' ')
|
145
|
+
|
146
|
+
search(search_string, question_nb, options)
|
147
|
+
end
|
148
|
+
|
149
|
+
def help(opts)
|
150
|
+
puts opts.banner + "\n\n"
|
151
|
+
puts "\t<search_string>: Search Stack Overflow for a combination of words"
|
152
|
+
puts "\t<question_id>: (Optional) Display the question with this #id from the search results"
|
153
|
+
end
|
154
|
+
|
155
|
+
def search(search_string, question_nb, options)
|
156
|
+
if options['offline']
|
157
|
+
api = API.new
|
158
|
+
#api = DB.new
|
159
|
+
else
|
160
|
+
api = API.new
|
161
|
+
end
|
162
|
+
|
163
|
+
questions = api.search(search_string)
|
164
|
+
if !questions or questions.length == 0
|
165
|
+
puts "No record found - Try a less specific (or sometimes, more specific) query"
|
166
|
+
return
|
167
|
+
end
|
168
|
+
|
169
|
+
if !question_nb
|
170
|
+
Formatter.new.questions_list(questions)
|
171
|
+
else
|
172
|
+
question = questions[question_nb.to_i - 1]
|
173
|
+
Formatter.new.question_viewer(question)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stackoverflow
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Xavier Antoviaque
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2012-03-24 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: Query StackOverflow from the command line (offline/online modes)
|
22
|
+
email: xavier@antoviaque.org
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- lib/stackoverflow.rb
|
31
|
+
has_rdoc: true
|
32
|
+
homepage: https://github.com/antoviaque/stack-overflow-command-line
|
33
|
+
licenses: []
|
34
|
+
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
version: "0"
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
segments:
|
54
|
+
- 0
|
55
|
+
version: "0"
|
56
|
+
requirements: []
|
57
|
+
|
58
|
+
rubyforge_project:
|
59
|
+
rubygems_version: 1.3.7
|
60
|
+
signing_key:
|
61
|
+
specification_version: 3
|
62
|
+
summary: Query StackOverflow from the command line (offline/online modes)
|
63
|
+
test_files: []
|
64
|
+
|