serel 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://secure.travis-ci.org/thomas-mcdonald/serel.png?branch=master)](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:
|