serel 1.0.0.rc1
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/README.md +10 -0
- data/lib/serel.rb +37 -0
- data/lib/serel/access_token.rb +54 -0
- data/lib/serel/answer.rb +75 -0
- data/lib/serel/badge.rb +89 -0
- data/lib/serel/base.rb +180 -0
- data/lib/serel/comment.rb +68 -0
- data/lib/serel/event.rb +25 -0
- data/lib/serel/exts.rb +88 -0
- data/lib/serel/inbox.rb +6 -0
- data/lib/serel/info.rb +21 -0
- data/lib/serel/post.rb +21 -0
- data/lib/serel/privilege.rb +6 -0
- data/lib/serel/question.rb +48 -0
- data/lib/serel/relation.rb +125 -0
- data/lib/serel/reputation.rb +6 -0
- data/lib/serel/request.rb +63 -0
- data/lib/serel/response.rb +5 -0
- data/lib/serel/revision.rb +9 -0
- data/lib/serel/site.rb +22 -0
- data/lib/serel/suggested_edit.rb +8 -0
- data/lib/serel/tag.rb +63 -0
- data/lib/serel/tag_score.rb +7 -0
- data/lib/serel/tag_synonym.rb +6 -0
- data/lib/serel/tag_wiki.rb +7 -0
- data/lib/serel/timeline.rb +7 -0
- data/lib/serel/user.rb +85 -0
- metadata +105 -0
data/README.md
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# Serel
|
2
|
+
[](http://travis-ci.org/thomas-mcdonald/serel)
|
3
|
+
|
4
|
+
Serel is an [AREL][1] inspired library developed for v2.0 of the [Stack Exchange API][2]. It's decent, you should try it too.
|
5
|
+
|
6
|
+
Read more on the [Serel site][3] or on Stack Apps.
|
7
|
+
|
8
|
+
[1]: https://github.com/rails/arel
|
9
|
+
[2]: https://api.stackexchange.com/
|
10
|
+
[3]: http://serel.tom.is
|
data/lib/serel.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Utilities
|
2
|
+
require 'logger'
|
3
|
+
require 'serel/exts'
|
4
|
+
require 'serel/relation'
|
5
|
+
require 'serel/response'
|
6
|
+
|
7
|
+
# Types
|
8
|
+
require 'serel/base'
|
9
|
+
require 'serel/access_token'
|
10
|
+
require 'serel/answer'
|
11
|
+
require 'serel/badge'
|
12
|
+
require 'serel/comment'
|
13
|
+
require 'serel/event'
|
14
|
+
require 'serel/inbox'
|
15
|
+
require 'serel/info'
|
16
|
+
require 'serel/post'
|
17
|
+
require 'serel/privilege'
|
18
|
+
require 'serel/question'
|
19
|
+
require 'serel/reputation'
|
20
|
+
require 'serel/request'
|
21
|
+
require 'serel/revision'
|
22
|
+
require 'serel/site'
|
23
|
+
require 'serel/suggested_edit'
|
24
|
+
require 'serel/tag'
|
25
|
+
require 'serel/tag_score'
|
26
|
+
require 'serel/tag_synonym'
|
27
|
+
require 'serel/tag_wiki'
|
28
|
+
require 'serel/user'
|
29
|
+
|
30
|
+
# HTTP magic
|
31
|
+
require 'cgi'
|
32
|
+
require 'json'
|
33
|
+
require 'net/http'
|
34
|
+
require 'uri'
|
35
|
+
require 'zlib'
|
36
|
+
|
37
|
+
class Serel::NoAPIKeyError < StandardError; end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Serel
|
2
|
+
# AccessToken is a special helper class designed to make it easier to use access tokens.
|
3
|
+
#
|
4
|
+
# It doesn't handle the retrieval of the token, merely provides a way to get it into Serel.
|
5
|
+
# Creating a new AccessToken provides a couple of helper methods to access authorized methods and
|
6
|
+
# identify who a user is.
|
7
|
+
class AccessToken < Base
|
8
|
+
attributes :token
|
9
|
+
finder_methods :none
|
10
|
+
|
11
|
+
# Create a new instance of AccessToken.
|
12
|
+
# @param [String] token The access token you wish to use.
|
13
|
+
# @return [Serel::AccessToken] An AccessToken intialized with the token.
|
14
|
+
def initialize(token)
|
15
|
+
@data = {}
|
16
|
+
@data[:token] = token
|
17
|
+
end
|
18
|
+
|
19
|
+
# Retrieve the users' inbox items.
|
20
|
+
# Serel::AccessToken.new(token).inbox.request
|
21
|
+
# Serel::AccessToken.new(token).scoping_methods.inbox.request
|
22
|
+
#
|
23
|
+
# This is a scoping method and can be combined with other scoping methods.
|
24
|
+
# @return [Serel::Response] A list of {Serel::Inbox Inbox} items wrapped in our Response wrapper.
|
25
|
+
def inbox
|
26
|
+
type(:inbox).access_token(self.token).url("me/inbox")
|
27
|
+
end
|
28
|
+
|
29
|
+
# Retrieve the users' unread inbox items.
|
30
|
+
# Serel::AccessToken.new(token).unread_inbox.request
|
31
|
+
#
|
32
|
+
# This is a scoping method and can be combined with other scoping methods.
|
33
|
+
# @return [Serel::Response] A list of {Serel::Inbox Inbox} wrapped in our Response wrapper.
|
34
|
+
def unread_inbox
|
35
|
+
type(:inbox).access_token(self.token).url("me/inbox/unread")
|
36
|
+
end
|
37
|
+
|
38
|
+
# Invalidates the access token
|
39
|
+
# Serel::AccessToken.new(token).invalidate
|
40
|
+
def invalidate
|
41
|
+
network.url("access-tokens/#{token}/invalidate").request
|
42
|
+
end
|
43
|
+
|
44
|
+
# Retrieve the user associated with this access token.
|
45
|
+
# Serel::AccessToken.new(token).user
|
46
|
+
#
|
47
|
+
# This does not return a {Response} object, rather it directly returns the User.
|
48
|
+
#
|
49
|
+
# @return [Serel::User] The user associated with the access token.
|
50
|
+
def user
|
51
|
+
type(:user, :singular).access_token(self.token).url("me").request
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/serel/answer.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
module Serel
|
2
|
+
# The Answer class represents a Stack Exchange answer object.
|
3
|
+
#
|
4
|
+
# == Finding answers
|
5
|
+
#
|
6
|
+
# There are a few different ways of retrieving answers: +all+, +get+ & +find+, as well as the scopes
|
7
|
+
# on other classes.
|
8
|
+
#
|
9
|
+
# === all
|
10
|
+
# +Serel::Answer.all+
|
11
|
+
#
|
12
|
+
# This should probably never be used. +all+ loads _every_ answer on the site, which, for many sites,
|
13
|
+
# involves retrieving lots of pages. However, it's there. Feel free to use it.
|
14
|
+
#
|
15
|
+
# == find
|
16
|
+
# +Serel::Answer.find(id)+
|
17
|
+
#
|
18
|
+
# Find an answer by its ID.
|
19
|
+
#
|
20
|
+
# == get
|
21
|
+
# +Serel::Answer.get+
|
22
|
+
#
|
23
|
+
# Retrieves answers, applying (any) scope that has been set. In reality this won't be called as given
|
24
|
+
# above, rather it will called at the end of a chain of scoping functions, e.g:
|
25
|
+
#
|
26
|
+
# +Serel::Answer.pagesize(100).sort(:votes).get+
|
27
|
+
#
|
28
|
+
# which would return the top 100 answers on the site.
|
29
|
+
#
|
30
|
+
# == {Question#answers}
|
31
|
+
# +Serel::Question.find(id).answers.get+
|
32
|
+
#
|
33
|
+
# Retrieves the answers on a particular question. The call to +answers+ returns a {Relation}
|
34
|
+
# object, which accepts all the usual relation scopes. This is true for all of the methods listed
|
35
|
+
# below.
|
36
|
+
#
|
37
|
+
# == {User#answers}
|
38
|
+
# +Serel::User.find(id).answers.get+
|
39
|
+
#
|
40
|
+
# Retrieves answers by a given user
|
41
|
+
class Answer < Base
|
42
|
+
attributes :answer_id, :body, :community_owned_date, :creation_date, :down_vote_count, :is_accepted, :last_activity_date, :last_edit_date, :link, :locked_date, :question_id, :score, :title, :up_vote_count
|
43
|
+
alias :id :answer_id
|
44
|
+
|
45
|
+
associations :comments => :comment, :owner => :user
|
46
|
+
finder_methods :every
|
47
|
+
|
48
|
+
# Get the comments on an answer.
|
49
|
+
# @return [Serel::Relation] A {Comment} relation scoped to the answer.
|
50
|
+
def comments
|
51
|
+
type(:comment).url("answers/#{id}/comments")
|
52
|
+
end
|
53
|
+
|
54
|
+
# Get the question this answer answers.
|
55
|
+
#
|
56
|
+
# Note that this method returns a question not wrapped in the {Response} wrapper.
|
57
|
+
#
|
58
|
+
# @return [Serel::Question] The parent {Question} for this answer.
|
59
|
+
def question
|
60
|
+
type(:question, :singular).url("questions/#{question_id}").request
|
61
|
+
end
|
62
|
+
|
63
|
+
# Get the revisions on an answer.
|
64
|
+
# @return [Serel::Relation] A {Revision} relation scoped to the answer.
|
65
|
+
def revisions
|
66
|
+
type(:revision).url("posts/#{id}/revisions")
|
67
|
+
end
|
68
|
+
|
69
|
+
# Get the suggested edits on an answer
|
70
|
+
# @return [Serel::Relation] A {SuggestedEdit} relation scoped to the answer.
|
71
|
+
def suggested_edits
|
72
|
+
type(:suggested_edit).url("posts/#{id}/suggested-edits")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/serel/badge.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
module Serel
|
2
|
+
# The Badge class represents a Stack Exchange badge object.
|
3
|
+
#
|
4
|
+
# == Finding Badges
|
5
|
+
#
|
6
|
+
# Badges can be retrieved using any of the standard finder methods: +all+, +get+ & +find+.
|
7
|
+
#
|
8
|
+
# === all
|
9
|
+
# Serel::Badge.all
|
10
|
+
#
|
11
|
+
# Unlike several of the other classes ({Answer}, {Comment}, {Question} etc) where the usage of +all+
|
12
|
+
# is recommended against, +all+ is probably the most useful finder method for retrieving badges, since
|
13
|
+
# a partial set of badges is probably not useful, and only a few pages have to be retrieved each time
|
14
|
+
# this is called. At the time of writing this, Stack Overflow required 17 pages to be retrieved,
|
15
|
+
# Server Fault required 2, and Gaming required 1, and shouldn't make too much of a dent in your quota.
|
16
|
+
#
|
17
|
+
# ==find
|
18
|
+
# Serel::Badge.find(id)
|
19
|
+
#
|
20
|
+
# Retrieves a badge or badges by ID.
|
21
|
+
#
|
22
|
+
# == get
|
23
|
+
# Serel::Badge.get
|
24
|
+
#
|
25
|
+
# Retrieves a page of badge results, applying any scopes that have previously been defined.
|
26
|
+
#
|
27
|
+
# == {named}
|
28
|
+
# Serel::Badge.named.request
|
29
|
+
#
|
30
|
+
# See the documentation for {named} below.
|
31
|
+
#
|
32
|
+
# == {recipients}
|
33
|
+
# Serel::Badge.recipients.request
|
34
|
+
# Serel::Badge.recipients(1).request
|
35
|
+
# Serel::Badge.recipients(1,2,3).request
|
36
|
+
#
|
37
|
+
# See the documentation for {recipients} below.
|
38
|
+
#
|
39
|
+
# == {tags}
|
40
|
+
# Serel::Badge.tags.request
|
41
|
+
#
|
42
|
+
# See the documentation for {tags} below.
|
43
|
+
class Badge < Base
|
44
|
+
attributes :badge_id, :badge_type, :description, :link, :name, :rank
|
45
|
+
alias :id :badge_id
|
46
|
+
|
47
|
+
associations :user => :user
|
48
|
+
finder_methods :every
|
49
|
+
|
50
|
+
# Retrieves only badges that are explicitly defined.
|
51
|
+
# This is a scoping method, meaning that it can be used around/with other scoping methods, for
|
52
|
+
# example:
|
53
|
+
# Serel::Badge.pagesize(5).named.request
|
54
|
+
# Serel::Badge.named.sort('gold').request
|
55
|
+
#
|
56
|
+
# @return [Serel::Relation] A relation scoped to the named URL.
|
57
|
+
def self.named
|
58
|
+
url("badges/name")
|
59
|
+
end
|
60
|
+
|
61
|
+
# Retrieves recently awarded badges.
|
62
|
+
# Serel::Badge.recipients.request
|
63
|
+
# Serel::Badge.recipients(1).request
|
64
|
+
# Serel::Badge.recipients(1,2,3).request
|
65
|
+
#
|
66
|
+
# This is a scoping method and can be combined with other scoping methods.
|
67
|
+
#
|
68
|
+
# @param [Array] ids The ID or IDs of the badges you want information on. Not passing an ID means
|
69
|
+
# all recently awarded badges will be returned.
|
70
|
+
# @return [Serel::Relation] A relation scoped to the correct recipients URL
|
71
|
+
def self.recipients(*ids)
|
72
|
+
if ids.length > 0
|
73
|
+
arg = ids.length > 1 ? ids.join(';') : ids.pop
|
74
|
+
url("badges/#{arg}/recipients")
|
75
|
+
else
|
76
|
+
url("badges/recipients")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Retrieves only badges that have been awarded due to participation in a tag.
|
81
|
+
# Serel::Badge.tags.request
|
82
|
+
#
|
83
|
+
# This is a scoping method and can be combined with other scoping methods.
|
84
|
+
# @return [Serel::Relation] A relation scoped to the tags URL.
|
85
|
+
def self.tags
|
86
|
+
url("badges/tags")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/serel/base.rb
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
module Serel
|
2
|
+
class Base
|
3
|
+
class_attribute :all_finder_methods, :api_key, :associations, :attributes, :finder_methods, :logger, :network, :site
|
4
|
+
self.all_finder_methods = %w(find get all)
|
5
|
+
|
6
|
+
def initialize(data)
|
7
|
+
@data = {}
|
8
|
+
attributes.each { |k| @data[k] = data[k.to_s] }
|
9
|
+
if associations
|
10
|
+
associations.each do |k,v|
|
11
|
+
if data[k.to_s]
|
12
|
+
@data[k] = find_constant(v).new(data[k.to_s])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Internal: Provides access to the internal data
|
19
|
+
# Rather than store data using attr_writers (problems) we use a hash.
|
20
|
+
def [](attr)
|
21
|
+
@data[attr]
|
22
|
+
end
|
23
|
+
|
24
|
+
def []=(attr, value)
|
25
|
+
@data[attr] = value
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Sets the global configuration values.
|
29
|
+
#
|
30
|
+
# site - The API site parameter that represents the site you wish to query.
|
31
|
+
# api_key - Your API key.
|
32
|
+
#
|
33
|
+
# Returns nothing.
|
34
|
+
def self.config(site, api_key = nil)
|
35
|
+
self.site = site.to_sym
|
36
|
+
self.api_key = api_key
|
37
|
+
self.logger = Logger.new(STDOUT)
|
38
|
+
self.logger.formatter = proc { |severity, datetime, progname, msg|
|
39
|
+
%([#{severity}][#{datetime.strftime("%Y-%m-%d %H:%M:%S")}] #{msg}\n)
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
# Public: Provides a nice and quick way to inspec the properties of a
|
44
|
+
# Serel instance inheriting from Serel::Base
|
45
|
+
#
|
46
|
+
# Returns a String representation of the class
|
47
|
+
def inspect
|
48
|
+
attribute_collector = {}
|
49
|
+
self.class.attributes.each { |attr| attribute_collector[attr] = self.send(attr) }
|
50
|
+
inspected_attributes = attribute_collector.select { |k,v| v != nil }.collect { |k,v| "@#{k}=#{v}" }.join(", ")
|
51
|
+
association_collector = {}
|
52
|
+
self.class.associations.each { |name, type| association_collector[name] = self[name] }
|
53
|
+
inspected_associations = association_collector.select { |k, v| v != nil }.collect { |k, v| "@#{k}=#<#{v.class}:#{v.object_id}>" }.join(", ")
|
54
|
+
"#<#{self.class}:#{self.object_id} #{inspected_attributes} #{inspected_associations}>"
|
55
|
+
end
|
56
|
+
alias :to_s :inspect
|
57
|
+
|
58
|
+
# Internal: Defines the attributes for a subclass of Serel::Base
|
59
|
+
#
|
60
|
+
# *splat - The Array or List of attributes for the class
|
61
|
+
#
|
62
|
+
# Returns nothing.
|
63
|
+
def self.attributes(*splat)
|
64
|
+
self.attributes = splat
|
65
|
+
splat.each do |meth|
|
66
|
+
define_method(meth) { self[meth.to_sym] }
|
67
|
+
define_method("#{meth}=") { |val| self[meth.to_sym] = val }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Internal: Defines the associations for a subclass of Serel::Base
|
72
|
+
#
|
73
|
+
# options - The Hash options used to define associations
|
74
|
+
# { :name => :type }
|
75
|
+
# :name - The Symbol name that this association will be found as in the data
|
76
|
+
# :type - The Symbol type that this association should be parsed as
|
77
|
+
#
|
78
|
+
# Returns nothing.
|
79
|
+
def self.associations(options = {})
|
80
|
+
self.associations = options
|
81
|
+
options.each do |meth, type|
|
82
|
+
unless self.respond_to?(meth)
|
83
|
+
define_method(meth) { self[meth.to_sym] }
|
84
|
+
end
|
85
|
+
define_method("#{meth}=") { |val| self[meth.to_sym] = val }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Public: Creates a new relation scoped to the class
|
90
|
+
#
|
91
|
+
# klass - the name of the class to scope the relation to
|
92
|
+
#
|
93
|
+
# Returns an instance of Serel::Relation
|
94
|
+
def self.new_relation(klass = nil, qty = :singular)
|
95
|
+
klass = name.split('::').last.to_snake unless klass
|
96
|
+
Serel::Relation.new(klass.to_s, qty)
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
# Public: Sets the finder methods available to this type
|
101
|
+
#
|
102
|
+
# *splat - The Array of methods this class accepts. Also accepts :every,
|
103
|
+
# which indicates that all finder methods are valid
|
104
|
+
#
|
105
|
+
# Returns nothing of value
|
106
|
+
def self.finder_methods(*splat)
|
107
|
+
self.finder_methods = splat
|
108
|
+
end
|
109
|
+
|
110
|
+
# Denotes that a class does not need the site parameter
|
111
|
+
def self.network_wide
|
112
|
+
self.network = true
|
113
|
+
end
|
114
|
+
|
115
|
+
# Public: Provides an interface to show which methods this class responds to
|
116
|
+
#
|
117
|
+
# method_sym - The Symbol representation of the method you are interested in
|
118
|
+
# include_private - Whether to include private methods. We ignore this, but
|
119
|
+
# it is part of how the core library handles respond_to
|
120
|
+
#
|
121
|
+
# Returns Boolean indicating whether the class responds to it or not
|
122
|
+
def self.respond_to?(method_sym, include_private = true)
|
123
|
+
if self.all_finder_methods.include?(method_sym.to_s)
|
124
|
+
if (self.finder_methods.include?(:every)) || (self.finder_methods.include?(method_sym))
|
125
|
+
true
|
126
|
+
else
|
127
|
+
false
|
128
|
+
end
|
129
|
+
else
|
130
|
+
super(method_sym, include_private)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def all
|
135
|
+
if self.respond_to?(:all)
|
136
|
+
new_relation.all
|
137
|
+
else
|
138
|
+
raise NoMethodError
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def find(id)
|
143
|
+
if self.respond_to?(:find)
|
144
|
+
new_relation.find(id)
|
145
|
+
else
|
146
|
+
raise NoMethodError
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def get
|
151
|
+
if self.respond_to?(:get)
|
152
|
+
new_relation.get
|
153
|
+
else
|
154
|
+
raise NoMethodError
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
## Pass these methods direct to a new Relation
|
159
|
+
# TODO: clean these up
|
160
|
+
%w(access_token filter fromdate inname intitle min max nottagged order page pagesize since sort tagged title todate url).each do |meth|
|
161
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
162
|
+
def self.#{meth}(val)
|
163
|
+
new_relation.#{meth}(val)
|
164
|
+
end
|
165
|
+
RUBY
|
166
|
+
end
|
167
|
+
def network; self.class.new_relation.network; end
|
168
|
+
def self.request(path, type = nil); new_relation.request(path, type); end
|
169
|
+
def type(klass, qty = :plural)
|
170
|
+
self.class.new_relation(klass, qty)
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.method_missing(sym, *attrs, &block)
|
174
|
+
# TODO: see if the new_relation responds to the method being called
|
175
|
+
# requires a rewrite of how method_missing works on the
|
176
|
+
# relation side.
|
177
|
+
new_relation.send(sym, *attrs, &block)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Serel
|
2
|
+
# The Comment class represents a Stack Exchange comment object.
|
3
|
+
#
|
4
|
+
# == Finding Comments
|
5
|
+
#
|
6
|
+
# Comments can be retrieved using any of the standard finder methods: +all+, +get+ & +find+.
|
7
|
+
#
|
8
|
+
# === all
|
9
|
+
# Serel::Comment.all
|
10
|
+
#
|
11
|
+
# It is recommended that you don't use this method, since most of the time it is overkill.
|
12
|
+
# Automatically paginating through all the comments on a site, even 100 comments at a time, will take
|
13
|
+
# a long time and a lot of requests. It is almost certainly better to use one of the other finder
|
14
|
+
# methods, but this is here for the sake of completion. If you need to access every comment, the data
|
15
|
+
# dump and data explorer are your friends.
|
16
|
+
#
|
17
|
+
# ==find
|
18
|
+
# Serel::Comment.find(id)
|
19
|
+
#
|
20
|
+
# Retrieves a comment or comments by ID.
|
21
|
+
#
|
22
|
+
# == get
|
23
|
+
# Serel::Comment.get
|
24
|
+
#
|
25
|
+
# Retrieves a page of comment results, applying any scopes that have previously been defined.
|
26
|
+
#
|
27
|
+
# == {Answer#comments}
|
28
|
+
# Serel::Answer.find(id).comments.request
|
29
|
+
#
|
30
|
+
# Retrieves a page of comments on the specified answer.
|
31
|
+
#
|
32
|
+
# == {Post#comments}
|
33
|
+
# Serel::Post.find(id).comments.request
|
34
|
+
#
|
35
|
+
# Retrieves a page of comments on the specified post.
|
36
|
+
#
|
37
|
+
# == {Question#comments}
|
38
|
+
# Serel::Question.find(id).comments.request
|
39
|
+
#
|
40
|
+
# Retrieves a page of comments on the specified question.
|
41
|
+
#
|
42
|
+
# == {User#comments}
|
43
|
+
# Serel::User.find(id).comments.request
|
44
|
+
# Serel::User.find(id).comments(other_id).request
|
45
|
+
#
|
46
|
+
# Retrieves a page of comments by the specified user. If the optional +other_id+ parameter is passed
|
47
|
+
# then only comments in reply to +other_id+ are included.
|
48
|
+
#
|
49
|
+
# == {User#mentioned}
|
50
|
+
# Serel::User.find(id).mentioned.request
|
51
|
+
#
|
52
|
+
# Retrieves a page of comments mentioning the specified user.
|
53
|
+
class Comment < Base
|
54
|
+
attributes :comment_id, :body, :creation_date, :edited, :link, :post_id, :post_type, :score
|
55
|
+
alias :id :comment_id
|
56
|
+
|
57
|
+
associations :owner => :user, :reply_to_user => :user
|
58
|
+
finder_methods :every
|
59
|
+
|
60
|
+
# Retrieves the post that this comment is on.
|
61
|
+
#
|
62
|
+
# Note that this returns the post, rather than returning a {Serel::Response} wrapped array.
|
63
|
+
# @return [Serel::Post] The post the comment is on.
|
64
|
+
def post
|
65
|
+
type(:post, :singular).url("posts/#{post_id}").request
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/serel/event.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Serel
|
2
|
+
# The Event class represents a Stack Exchange event.
|
3
|
+
#
|
4
|
+
# == Finding events
|
5
|
+
#
|
6
|
+
# You can use the +all+ and +get+ finder methods to retrieve events. Both of these require
|
7
|
+
# authentication, so {Relation#access_token access_token} must be called before the finders.
|
8
|
+
#
|
9
|
+
# === all
|
10
|
+
# Serel::Event.access_token(token).all
|
11
|
+
#
|
12
|
+
# Retrieves (by default) all the events on the site in the last 5 minutes. The +since+ method can
|
13
|
+
# be called to change this return events upto 15 minutes ago. This method is safe to call, since even
|
14
|
+
# on Stack Overflow only roughly 3 pages of events are generated every 5 minutes.
|
15
|
+
#
|
16
|
+
# == get
|
17
|
+
# Serel::Event.access_token(token).get
|
18
|
+
#
|
19
|
+
# Retrieves a page of Events. As per the +all+ method above, this defaults to events in the last 5
|
20
|
+
# minutes.
|
21
|
+
class Event < Base
|
22
|
+
attributes :event_type, :event_id, :creation_date, :link, :excerpt
|
23
|
+
finder_methods :all, :get
|
24
|
+
end
|
25
|
+
end
|
data/lib/serel/exts.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# TODO: make file less ugly.
|
2
|
+
|
3
|
+
def remove_possible_method(method)
|
4
|
+
if method_defined?(method) || private_method_defined?(method)
|
5
|
+
remove_method(method)
|
6
|
+
end
|
7
|
+
rescue NameError
|
8
|
+
# If the requested method is defined on a superclass or included module,
|
9
|
+
# method_defined? returns true but remove_method throws a NameError.
|
10
|
+
# Ignore this.
|
11
|
+
end
|
12
|
+
|
13
|
+
def singleton_class?
|
14
|
+
!name || '' == name
|
15
|
+
end
|
16
|
+
|
17
|
+
def singleton_class
|
18
|
+
class << self
|
19
|
+
self
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def class_attribute(*attrs)
|
24
|
+
if attrs.last.is_a?(Hash)
|
25
|
+
options = attrs.pop
|
26
|
+
else
|
27
|
+
options = {}
|
28
|
+
end
|
29
|
+
instance_reader = options.fetch(:instance_reader, true)
|
30
|
+
instance_writer = options.fetch(:instance_writer, true)
|
31
|
+
|
32
|
+
attrs.each do |name|
|
33
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
34
|
+
def self.#{name}() nil end
|
35
|
+
def self.#{name}?() !!#{name} end
|
36
|
+
|
37
|
+
def self.#{name}=(val)
|
38
|
+
singleton_class.class_eval do
|
39
|
+
remove_possible_method(:#{name})
|
40
|
+
define_method(:#{name}) { val }
|
41
|
+
end
|
42
|
+
|
43
|
+
if singleton_class?
|
44
|
+
class_eval do
|
45
|
+
remove_possible_method(:#{name})
|
46
|
+
def #{name}
|
47
|
+
defined?(@#{name}) ? @#{name} : singleton_class.#{name}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
val
|
52
|
+
end
|
53
|
+
|
54
|
+
if instance_reader
|
55
|
+
remove_possible_method :#{name}
|
56
|
+
def #{name}
|
57
|
+
defined?(@#{name}) ? @#{name} : self.class.#{name}
|
58
|
+
end
|
59
|
+
|
60
|
+
def #{name}?
|
61
|
+
!!#{name}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
RUBY
|
65
|
+
|
66
|
+
attr_writer name if instance_writer
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def find_constant(symbol)
|
71
|
+
str = symbol.to_s.dup
|
72
|
+
match = /_([a-z])/.match(str)
|
73
|
+
if match
|
74
|
+
str.gsub!(match[0], match[1].upcase)
|
75
|
+
end
|
76
|
+
str[0] = str[0].upcase
|
77
|
+
Serel.const_get(str)
|
78
|
+
end
|
79
|
+
|
80
|
+
class String
|
81
|
+
# Yeah, it's #underscore in Rails, but that's not half as fun.
|
82
|
+
def to_snake
|
83
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
84
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
85
|
+
tr("-", "_").
|
86
|
+
downcase
|
87
|
+
end
|
88
|
+
end
|
data/lib/serel/inbox.rb
ADDED
data/lib/serel/info.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Serel
|
2
|
+
# The Info class represents information about a Stack Exchange site.
|
3
|
+
#
|
4
|
+
# == Getting info
|
5
|
+
# The +/info+ route accepts no parameters and requires no IDs, so the default finder methods cannot be
|
6
|
+
# used. We therefore have a custom finder defined, {.get_info}.
|
7
|
+
class Info < Base
|
8
|
+
attributes :total_questions, :total_unanswered, :total_accepted, :total_answers, :questions_per_minute, :answers_per_minute, :total_comments, :total_votes, :total_badges, :badges_per_minute, :total_users, :new_active_users, :api_revision
|
9
|
+
associations :site => :site
|
10
|
+
finder_methods :none
|
11
|
+
|
12
|
+
# Gets information about the current site.
|
13
|
+
#
|
14
|
+
# It is best practice to aggresively cache the returned values with a TTL of at least 3600 seconds.
|
15
|
+
#
|
16
|
+
# @return [Serel::Info] Information about the site
|
17
|
+
def self.get_info
|
18
|
+
new_relation(:info, :singular).url("info").request
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/serel/post.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Serel
|
2
|
+
class Post < Base
|
3
|
+
attributes :post_id, :post_type, :body, :creation_date, :last_activity_date, :last_edit_date, :score, :up_vote_count, :down_vote_count
|
4
|
+
alias :id :post_id
|
5
|
+
|
6
|
+
associations :comments => :comment, :owner => :user
|
7
|
+
finder_methods :every
|
8
|
+
|
9
|
+
def comments
|
10
|
+
type(:comment).url("posts/#{id}/comments")
|
11
|
+
end
|
12
|
+
|
13
|
+
def revisions
|
14
|
+
type(:revision).url("posts/#{id}/revisions")
|
15
|
+
end
|
16
|
+
|
17
|
+
def suggested_edits
|
18
|
+
type(:suggested_edit).url("posts/#{id}/suggested-edits")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Serel
|
2
|
+
class Question < Base
|
3
|
+
attributes :question_id, :accepted_answer_id, :answer_count, :body, :bounty_amount, :bounty_closes_date, :closed_reason, :community_owned_date, :creation_date, :down_vote_count, :favourite_count, :last_activity_date, :last_edit_date, :link, :locked_date, :migrated_to, :migrated_from, :protected_date, :score, :tags, :title, :up_vote_count, :view_count
|
4
|
+
associations :answers => :answer, :comments => :comment, :owner => :user
|
5
|
+
alias :id :question_id
|
6
|
+
finder_methods :every
|
7
|
+
|
8
|
+
def self.featured
|
9
|
+
url("questions/featured")
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.search
|
13
|
+
url("search")
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.similar
|
17
|
+
url("similar")
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.unanswered
|
21
|
+
url("questions/unanswered")
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.no_answers
|
25
|
+
url("questions/no-answers")
|
26
|
+
end
|
27
|
+
|
28
|
+
def answers
|
29
|
+
type(:answer).url("questions/#{@id}/answers")
|
30
|
+
end
|
31
|
+
|
32
|
+
def comments
|
33
|
+
type(:comment).url("questions/#{@id}/comments")
|
34
|
+
end
|
35
|
+
|
36
|
+
def linked
|
37
|
+
type(:question).url("questions/#{@id}/linked")
|
38
|
+
end
|
39
|
+
|
40
|
+
def related
|
41
|
+
type(:question).url("questions/#{@id}/related")
|
42
|
+
end
|
43
|
+
|
44
|
+
def timeline
|
45
|
+
type(:timeline).url("questions/#{@id}/timeline")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module Serel
|
2
|
+
class Relation
|
3
|
+
attr_reader :type, :klass, :qty
|
4
|
+
|
5
|
+
def initialize(type, qty)
|
6
|
+
@type = type
|
7
|
+
@klass = find_constant(type)
|
8
|
+
@scope = {
|
9
|
+
api_key: Serel::Base.api_key,
|
10
|
+
site: Serel::Base.site
|
11
|
+
}
|
12
|
+
@qty = qty
|
13
|
+
end
|
14
|
+
|
15
|
+
# Public: Merges two relation objects together. This is used in our awesome
|
16
|
+
# new scoping engine!
|
17
|
+
#
|
18
|
+
# relation - A Serel::Relation object with the same base class as the
|
19
|
+
# current relation
|
20
|
+
#
|
21
|
+
# Returns self
|
22
|
+
def merge(relation)
|
23
|
+
if relation.instance_variable_get(:@type) != @type
|
24
|
+
raise ArgumentError, 'You cannot merge two relation objects based on different classes'
|
25
|
+
end
|
26
|
+
@scope.merge!(relation.scoping)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Scoping returns our internal scope defition. Things like url etc.
|
30
|
+
def scoping
|
31
|
+
@scope
|
32
|
+
end
|
33
|
+
|
34
|
+
def new_relation
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def method_missing(sym, *attrs, &block)
|
39
|
+
# If the base relation class responds to the method, call
|
40
|
+
# it and merge in the resulting relation scope
|
41
|
+
if @klass.respond_to?(sym)
|
42
|
+
relation = @klass.send(sym, *attrs, &block)
|
43
|
+
merge(relation)
|
44
|
+
self
|
45
|
+
end
|
46
|
+
super(sym, *attrs, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
#
|
51
|
+
#
|
52
|
+
# Scope methods
|
53
|
+
%w(access_token filter fromdate inname intitle min max nottagged order page pagesize since sort tagged title todate url).each do |meth|
|
54
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
55
|
+
def #{meth}(val)
|
56
|
+
@scope[:#{meth}] = val.to_s
|
57
|
+
self
|
58
|
+
end
|
59
|
+
RUBY
|
60
|
+
end
|
61
|
+
|
62
|
+
def network
|
63
|
+
@network = true
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
#
|
69
|
+
#
|
70
|
+
# Finder methods
|
71
|
+
def all
|
72
|
+
if klass.respond_to?(:all)
|
73
|
+
all_helper(1)
|
74
|
+
else
|
75
|
+
raise NoMethodError
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Finds an object by an ID or list of IDs. For example:
|
80
|
+
# Serel::Answer.find(1) # Find the answer with an ID of 1
|
81
|
+
# Serel::Answer.find(1, 2, 3) # Find the answers with IDs of 1, 2 & 3
|
82
|
+
#
|
83
|
+
# @param [Array] ids The ID or IDs of the objects you want returning.
|
84
|
+
# @return [Serel::Response] The data returned by the Stack Exchange API, parsed and pushed into our
|
85
|
+
# handy response wrapper.
|
86
|
+
def find(*ids)
|
87
|
+
if klass.respond_to?(:find)
|
88
|
+
arg = ids.length > 1 ? ids.join(';') : ids.pop
|
89
|
+
url("#{@type}s/#{arg}").request
|
90
|
+
else
|
91
|
+
raise NoMethodError
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def get
|
96
|
+
if klass.respond_to?(:get)
|
97
|
+
url("#{@type}s").request
|
98
|
+
else
|
99
|
+
raise NoMethodError
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Request stuff
|
104
|
+
def request
|
105
|
+
if (klass.network || @network)
|
106
|
+
@scope[:network] = true
|
107
|
+
end
|
108
|
+
Serel::Request.new(@type, scoping, @qty).execute
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def all_helper(page)
|
114
|
+
response = page(page).pagesize(100).url("#{@type}s").request
|
115
|
+
# TODO: find a query that triggers backoff.
|
116
|
+
# if response.backoff
|
117
|
+
# Serel::Base.warn response.backoff
|
118
|
+
# end
|
119
|
+
if response.has_more
|
120
|
+
response.concat all_helper(page+1)
|
121
|
+
end
|
122
|
+
response
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Serel
|
2
|
+
class Request
|
3
|
+
def initialize(type, scoping, qty)
|
4
|
+
@type = type
|
5
|
+
@scope = scoping.dup
|
6
|
+
@site = @scope.delete :site
|
7
|
+
@api_key = @scope.delete :api_key
|
8
|
+
raise Serel::NoAPIKeyError, 'You must configure Serel with an API key before you can make requests' if @api_key == nil
|
9
|
+
@method = @scope.delete :url
|
10
|
+
@network = @scope.delete :network
|
11
|
+
@qty = qty
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute
|
15
|
+
build_query_string
|
16
|
+
build_request_path
|
17
|
+
make_request
|
18
|
+
end
|
19
|
+
|
20
|
+
def build_query_string
|
21
|
+
query_hash = @scope
|
22
|
+
unless @network
|
23
|
+
query_hash[:site] = @site
|
24
|
+
end
|
25
|
+
query_hash[:key] = @api_key
|
26
|
+
@query_string = query_hash.map { |k,v| "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}"}.join('&')
|
27
|
+
end
|
28
|
+
|
29
|
+
def build_request_path
|
30
|
+
@path = "/2.0/#{@method}?#{@query_string}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def make_request
|
34
|
+
Serel::Base.logger.info "Making request to #{@path}"
|
35
|
+
http = Net::HTTP.new('api.stackexchange.com', 443)
|
36
|
+
http.use_ssl = true
|
37
|
+
response = http.get(@path)
|
38
|
+
body = JSON.parse(response.body)
|
39
|
+
|
40
|
+
result = Serel::Response.new
|
41
|
+
|
42
|
+
# Set the values of the response wrapper attributes
|
43
|
+
%w(backoff error_id error_message error_name has_more page page_size quota_max quota_remaining total type).each do |attr|
|
44
|
+
result.send("#{attr}=", body[attr])
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set some response values we know about but SE might not send back
|
48
|
+
result.page ||= (@scope[:page] || 1)
|
49
|
+
result.page_size ||= (@scope[:pagesize] || 30)
|
50
|
+
|
51
|
+
# Insert into the response array the items returned
|
52
|
+
body["items"].each do |item|
|
53
|
+
result << find_constant(@type).new(item)
|
54
|
+
end
|
55
|
+
|
56
|
+
if (@qty == :plural) || (result.length > 1)
|
57
|
+
result
|
58
|
+
else
|
59
|
+
result.pop
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Serel
|
2
|
+
class Revision < Base
|
3
|
+
attributes :revision_guid, :revision_number, :revision_type, :post_type, :post_id, :comment, :creation_date, :is_rollback, :last_body, :last_title, :last_tags, :body, :title, :tags, :set_community_wiki
|
4
|
+
alias :id :revision_guid
|
5
|
+
|
6
|
+
associations :user => :user
|
7
|
+
finder_methods :find
|
8
|
+
end
|
9
|
+
end
|
data/lib/serel/site.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Serel
|
2
|
+
# The Site class represents a Stack Exchange site.
|
3
|
+
#
|
4
|
+
# == Finding Sites
|
5
|
+
#
|
6
|
+
# Sites can be retrieved using +all+ and +get+.
|
7
|
+
#
|
8
|
+
# === all
|
9
|
+
# Serel::Site.all
|
10
|
+
#
|
11
|
+
# Automatically paginates through the list of sites and returns an array of every Stack Exchange site.
|
12
|
+
#
|
13
|
+
# == get
|
14
|
+
# Serel::Site.get
|
15
|
+
#
|
16
|
+
# Retrieves a page of sites, applying any scopes that have previously been defined.
|
17
|
+
class Site < Base
|
18
|
+
attributes :site_type, :name, :logo_url, :api_site_parameter, :site_url, :audience, :icon_url, :aliases, :site_state, :styling, :closed_beta_date, :open_beta_date, :launch_date, :favicon_url, :related_sites, :twitter_account, :markdown_extensions
|
19
|
+
finder_methods :all, :get
|
20
|
+
network_wide
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module Serel
|
2
|
+
class SuggestedEdit < Base
|
3
|
+
attributes :suggested_edit_id, :post_id, :post_type, :body, :title, :tags, :comment, :creation_date, :approval_date, :rejection_date
|
4
|
+
alias :id :suggested_edit_id
|
5
|
+
associations :proposing_user => :user
|
6
|
+
finder_methods :every
|
7
|
+
end
|
8
|
+
end
|
data/lib/serel/tag.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
module Serel
|
2
|
+
class Tag < Base
|
3
|
+
attributes :name, :count, :is_required, :is_moderator_only, :user_id, :has_synonyms, :last_activity_date
|
4
|
+
finder_methods :all, :get
|
5
|
+
|
6
|
+
# Finds a tag by name
|
7
|
+
# @param [String] name The name of the tag you wish to find
|
8
|
+
# @return [Serel::Tag] The tag returned by the Stack Exchange API
|
9
|
+
def self.find_by_name(name)
|
10
|
+
url("tags/#{name}/info").request
|
11
|
+
end
|
12
|
+
|
13
|
+
# Retrieves tags which can only be added or removed by a moderator
|
14
|
+
# Serel::Tag.moderator_only.request
|
15
|
+
#
|
16
|
+
# This is a scoping method and can be combined with other scoping methods
|
17
|
+
# @return [Serel::Relation] A relation scoped to the moderator only URL.
|
18
|
+
def self.moderator_only
|
19
|
+
url("tags/moderator-only")
|
20
|
+
end
|
21
|
+
|
22
|
+
# Retrieves tags that are required on the site
|
23
|
+
# Serel::Tag.required.request
|
24
|
+
#
|
25
|
+
# This is a scoping method and can be combined with other scoping methods.
|
26
|
+
# @return [Serel::Relation] A relation scoped to the required URL.
|
27
|
+
def self.required
|
28
|
+
url("tags/required")
|
29
|
+
end
|
30
|
+
|
31
|
+
# Retrieves all the tag synonyms on the site
|
32
|
+
# Serel::Tag.synonyms.request
|
33
|
+
#
|
34
|
+
# This is a scoping method and can be combined with other scoping methods.
|
35
|
+
# @return [Serel::Relation] A relation scoped to {Serel::TagSynonym TagSynonym} and the synonym URL.
|
36
|
+
def self.synonyms
|
37
|
+
new_relation(:tag_synonym).url("tags/synonyms")
|
38
|
+
end
|
39
|
+
|
40
|
+
# Retrieves related tags.
|
41
|
+
# Serel::Tag.find(1).related.request
|
42
|
+
#
|
43
|
+
# This is a scoping method and can be combined with other scoping methods.
|
44
|
+
# @return [Serel::Relation] A relation scoped to the related URL
|
45
|
+
def related
|
46
|
+
type(:tag).url("tags/#{name}/related")
|
47
|
+
end
|
48
|
+
|
49
|
+
def top_answerers(period)
|
50
|
+
raise ArgumentError, 'period must be :all_time or :month' unless [:all_time, :month].include? period
|
51
|
+
type(:tag_score).url("tags/#{name}/top-answerers/#{period}")
|
52
|
+
end
|
53
|
+
|
54
|
+
def top_askers(period)
|
55
|
+
raise ArgumentError, 'period must be :all_time or :month' unless [:all_time, :month].include? period
|
56
|
+
type(:tag_score).url("tags/#{name}/top-askers/#{period}")
|
57
|
+
end
|
58
|
+
|
59
|
+
def wiki
|
60
|
+
type(:tag_wiki, :singular).url("tags/#{name}/wikis").request
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/serel/user.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
module Serel
|
2
|
+
class User < Base
|
3
|
+
attributes :user_id, :user_type, :creation_date, :display_name, :profile_image, :reputation, :reputation_change_day, :reputation_change_week, :reputation_change_month, :reputation_change_quarter, :reputation_change_year, :age, :last_access_date, :last_modified_date, :is_employee, :link, :website_url, :location, :account_id, :timed_penalty_date, :badge_counts, :question_counts, :answer_count, :up_vote_count, :down_vote_count, :about_me, :view_count, :accept_rate
|
4
|
+
alias :id :user_id
|
5
|
+
finder_methods :every
|
6
|
+
|
7
|
+
def self.moderators
|
8
|
+
url("users/moderators")
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.elected_moderators
|
12
|
+
url("users/moderators/elected")
|
13
|
+
end
|
14
|
+
|
15
|
+
def answers
|
16
|
+
type(:answer).url("users/#{id}/answers")
|
17
|
+
end
|
18
|
+
|
19
|
+
def badges
|
20
|
+
type(:badge).url("users/#{id}/badges")
|
21
|
+
end
|
22
|
+
|
23
|
+
def comments(to_id = nil)
|
24
|
+
if to_id
|
25
|
+
type(:comment).url("users/#{id}/comments/#{to_id}")
|
26
|
+
else
|
27
|
+
type(:comment).url("users/#{id}/comments")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def favorites
|
32
|
+
type(:question).url("users/#{id}/favorites")
|
33
|
+
end
|
34
|
+
|
35
|
+
def mentioned
|
36
|
+
type(:comment).url("users/#{id}/mentioned")
|
37
|
+
end
|
38
|
+
|
39
|
+
def privileges
|
40
|
+
type(:privilege).url("users/#{id}/privileges")
|
41
|
+
end
|
42
|
+
|
43
|
+
def questions
|
44
|
+
type(:question).url("users/#{id}/questions")
|
45
|
+
end
|
46
|
+
|
47
|
+
def questions_featured
|
48
|
+
type(:question).url("users/#{id}/questions/featured")
|
49
|
+
end
|
50
|
+
|
51
|
+
def questions_no_answers
|
52
|
+
type(:question).url("users/#{id}/questions/no-answers")
|
53
|
+
end
|
54
|
+
|
55
|
+
def questions_unaccepted
|
56
|
+
type(:question).url("users/#{id}/questions/unaccepted")
|
57
|
+
end
|
58
|
+
|
59
|
+
def questions_unanswered
|
60
|
+
type(:question).url("users/#{id}/questions/unanswered")
|
61
|
+
end
|
62
|
+
|
63
|
+
def reputation
|
64
|
+
type(:reputation).url("users/#{id}/reputation")
|
65
|
+
end
|
66
|
+
|
67
|
+
def suggested_edits
|
68
|
+
type(:suggested_edit).url("users/#{id}/suggested-edits")
|
69
|
+
end
|
70
|
+
|
71
|
+
def tags
|
72
|
+
type(:tag).url("users/#{id}/tags")
|
73
|
+
end
|
74
|
+
|
75
|
+
def top_answers_on(*tags)
|
76
|
+
arg = tags.length > 1 ? tags.join(";") : tags.pop
|
77
|
+
type(:answer).url("users/#{id}/tags/#{arg}/top-answers")
|
78
|
+
end
|
79
|
+
|
80
|
+
def top_questions_on(*tags)
|
81
|
+
arg = tags.length > 1 ? tags.join(";") : tags.pop
|
82
|
+
type(:question).url("users/#{id}/tags/#{arg}/top-questions")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: serel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.rc1
|
5
|
+
prerelease: 6
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Thomas McDonald
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-02-29 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70120458988780 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70120458988780
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: vcr
|
27
|
+
requirement: &70120458987680 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70120458987680
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: webmock
|
38
|
+
requirement: &70120458986560 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70120458986560
|
47
|
+
description:
|
48
|
+
email: tom@conceptcoding.co.uk
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- lib/serel/access_token.rb
|
54
|
+
- lib/serel/answer.rb
|
55
|
+
- lib/serel/badge.rb
|
56
|
+
- lib/serel/base.rb
|
57
|
+
- lib/serel/comment.rb
|
58
|
+
- lib/serel/event.rb
|
59
|
+
- lib/serel/exts.rb
|
60
|
+
- lib/serel/inbox.rb
|
61
|
+
- lib/serel/info.rb
|
62
|
+
- lib/serel/post.rb
|
63
|
+
- lib/serel/privilege.rb
|
64
|
+
- lib/serel/question.rb
|
65
|
+
- lib/serel/relation.rb
|
66
|
+
- lib/serel/reputation.rb
|
67
|
+
- lib/serel/request.rb
|
68
|
+
- lib/serel/response.rb
|
69
|
+
- lib/serel/revision.rb
|
70
|
+
- lib/serel/site.rb
|
71
|
+
- lib/serel/suggested_edit.rb
|
72
|
+
- lib/serel/tag.rb
|
73
|
+
- lib/serel/tag_score.rb
|
74
|
+
- lib/serel/tag_synonym.rb
|
75
|
+
- lib/serel/tag_wiki.rb
|
76
|
+
- lib/serel/timeline.rb
|
77
|
+
- lib/serel/user.rb
|
78
|
+
- lib/serel.rb
|
79
|
+
- README.md
|
80
|
+
homepage: http://serel.tom.is
|
81
|
+
licenses: []
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ! '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ! '>'
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: 1.3.1
|
98
|
+
requirements: []
|
99
|
+
rubyforge_project:
|
100
|
+
rubygems_version: 1.8.12
|
101
|
+
signing_key:
|
102
|
+
specification_version: 3
|
103
|
+
summary: A Ruby library for the Stack Exchange API
|
104
|
+
test_files: []
|
105
|
+
has_rdoc:
|