livefyre-mashable 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +3 -0
- data.tar.gz.sig +0 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +169 -0
- data/Rakefile +12 -0
- data/app/assets/javascripts/livefyre.js +180 -0
- data/app/assets/javascripts/livefyre.js.coffee +109 -0
- data/gem-public_cert.pem +21 -0
- data/lib/livefyre.rb +54 -0
- data/lib/livefyre/activity.rb +43 -0
- data/lib/livefyre/client.rb +141 -0
- data/lib/livefyre/comment.rb +190 -0
- data/lib/livefyre/controller_extensions.rb +82 -0
- data/lib/livefyre/conversation.rb +133 -0
- data/lib/livefyre/domain.rb +220 -0
- data/lib/livefyre/engine.rb +6 -0
- data/lib/livefyre/helpers.rb +57 -0
- data/lib/livefyre/model_extensions.rb +64 -0
- data/lib/livefyre/site.rb +237 -0
- data/lib/livefyre/user.rb +162 -0
- data/lib/livefyre/version.rb +3 -0
- data/livefyre.gemspec +39 -0
- data/railties/railtie.rb +9 -0
- data/spec/livefyre/client_spec.rb +104 -0
- data/spec/livefyre/domain_spec.rb +294 -0
- data/spec/livefyre/livefyre_spec.rb +30 -0
- data/spec/livefyre/model_spec.rb +96 -0
- data/spec/livefyre/site_spec.rb +212 -0
- data/spec/livefyre/user_spec.rb +111 -0
- data/spec/spec_helper.rb +26 -0
- metadata +261 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
module Livefyre
|
2
|
+
module Model
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# Public: Ping Livefyre to refresh this user's record
|
6
|
+
def refresh_livefyre
|
7
|
+
if _livefyre_callback
|
8
|
+
_livefyre_callback.call( self, self.send(:_livefyre_id) )
|
9
|
+
else
|
10
|
+
Livefyre::User.refresh( self.send(:_livefyre_id) )
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def update_livefyre_if_fields_changed
|
17
|
+
if updates = _livefyre_options[:update_on]
|
18
|
+
updates.each do |field|
|
19
|
+
if send("#{field}_changed?")
|
20
|
+
refresh_livefyre
|
21
|
+
break
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def _livefyre_options
|
28
|
+
self.class.instance_variable_get("@livefyre_options")
|
29
|
+
end
|
30
|
+
|
31
|
+
def _livefyre_callback
|
32
|
+
self.class.instance_variable_get("@livefyre_update_block")
|
33
|
+
end
|
34
|
+
|
35
|
+
def _livefyre_id
|
36
|
+
livefyre_id = self.id
|
37
|
+
if _livefyre_options[:id]
|
38
|
+
livefyre_id = self.send(_livefyre_options[:id])
|
39
|
+
end
|
40
|
+
livefyre_id
|
41
|
+
end
|
42
|
+
|
43
|
+
public
|
44
|
+
|
45
|
+
module ClassMethods
|
46
|
+
# Public: Adds callback handlers and additional methods for treating this record as a user record.
|
47
|
+
#
|
48
|
+
# options - [Hash] of options to initialize behavior with
|
49
|
+
# :update_on - [Array<Symbol>] List of fields which should trigger a Livefyre update when they're updated.
|
50
|
+
# :id - [Symbol] Name of the method to use for determining this record's livefyre ID. If not given, #id is used.
|
51
|
+
#
|
52
|
+
# Examples
|
53
|
+
#
|
54
|
+
# livefyre_user :update_on => [:email, :first_name, :last_name, :username, :picture_url], :id => :custom_livefyre_id
|
55
|
+
#
|
56
|
+
# Returns [nil]
|
57
|
+
def livefyre_user(options = {}, &block)
|
58
|
+
@livefyre_options = options
|
59
|
+
@livefyre_update_block = block
|
60
|
+
after_save :update_livefyre_if_fields_changed
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
module Livefyre
|
2
|
+
# Public: Exception representing a failure to validate a signature
|
3
|
+
class InvalidSignatureException < Exception; end
|
4
|
+
|
5
|
+
# Public: An object representing a Livefyre site belonging to a Livefyre domain
|
6
|
+
class Site
|
7
|
+
attr_accessor :client, :secret, :options, :id
|
8
|
+
|
9
|
+
# Public: Create a new Site
|
10
|
+
def initialize(id, client = nil, options = {})
|
11
|
+
@id = id
|
12
|
+
@client = client || Livefyre.client
|
13
|
+
@options = options
|
14
|
+
@secret = options["api_secret"]
|
15
|
+
end
|
16
|
+
|
17
|
+
# Public: Search conversations on this domain
|
18
|
+
#
|
19
|
+
# query - string to query for
|
20
|
+
# options - [Hash] of options
|
21
|
+
# :fields - list of fields to search. Default [:article, :title, :body]
|
22
|
+
# :sort - Sort order for options. Valid values are [:relevance, :created, :updated, :hotness, :ncomments]. Default is :relevance
|
23
|
+
# :fields - List of fields to return in the result. Valid values are: article_id, site_id, domain_id, title, published, updated, author, url, ncomment, nuser, annotation, nlp, hotness, hottest_value, hottest_time, peak, peak_value, peak_time, comments:5, users:5, comment_state, hit_field, dispurl, relevancy
|
24
|
+
# :max - Maximum number of fields to return
|
25
|
+
# :since - [DateTime] Minimum date of results to return
|
26
|
+
# :until - [DateTime] Maximum date of results to return
|
27
|
+
# :page - Page of results to fetch. Default 1.
|
28
|
+
#
|
29
|
+
# Returns [Array<Conversation>] An array of matching conversations
|
30
|
+
# Raises [APIException] when response is not valid
|
31
|
+
def search_conversations(query, options = {})
|
32
|
+
options[:sites] = [self]
|
33
|
+
Domain.new(@client).search_conversations(query, options)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Public: Get a list of properties for this site
|
37
|
+
#
|
38
|
+
# reload - Force a reload when set
|
39
|
+
#
|
40
|
+
# Returns [Hash] Site properties
|
41
|
+
# Raises [APIException] when response is not valid
|
42
|
+
def properties(reload = false)
|
43
|
+
return @options unless @options.nil? or @options.empty? or reload
|
44
|
+
response = client.get "/site/#{id}/", {:actor_token => client.system_token}
|
45
|
+
if response.success?
|
46
|
+
@options = JSON.parse response.body
|
47
|
+
@secret = options["api_secret"]
|
48
|
+
@options
|
49
|
+
else
|
50
|
+
raise APIException.new(response.body)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Public: Fetches a feed of the site's latest activity.
|
55
|
+
#
|
56
|
+
# since_id - [Integer] If provided, will return feed items after the given feed item.
|
57
|
+
#
|
58
|
+
# Returns [Array<Activity>] List of feed activities
|
59
|
+
def feed(since_id = nil)
|
60
|
+
reload if secret.nil?
|
61
|
+
timestamp = Time.now.utc.to_i
|
62
|
+
sig = Base64.encode64 HMAC::SHA1.new(Base64.decode64 secret).update("sig_created=%s" % timestamp).digest
|
63
|
+
url = "/%s/" % ["site", id, "sync", since_id].compact.join("/")
|
64
|
+
response = client.get url, {:sig_created => timestamp, :sig => sig}
|
65
|
+
if response.success?
|
66
|
+
payload = JSON.parse(response.body).map {|item| Activity.new(client, item) }
|
67
|
+
else
|
68
|
+
raise APIException.new(response.body)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Public: Fetches the latest comments from this site
|
73
|
+
#
|
74
|
+
# since_id - [Integer] If provided, will return feed items after the given comment.
|
75
|
+
#
|
76
|
+
# Returns: [Array<Comment>] List of comment
|
77
|
+
def comments(since = nil)
|
78
|
+
feed(since).select(&:comment?).map(&:comment)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Public: Reload this site's properties from Livefyre
|
82
|
+
#
|
83
|
+
# Returns self
|
84
|
+
def reload
|
85
|
+
properties(true)
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
# Public: Set the postback URL for actions on this site
|
90
|
+
# See: https://github.com/Livefyre/livefyre-docs/wiki/Accessing-Site-Comment-Data
|
91
|
+
#
|
92
|
+
# url - [String] URL to use as the postback URL for actions
|
93
|
+
#
|
94
|
+
# Returns [Bool] true on success
|
95
|
+
# Raises: [APIException] when response is not valid
|
96
|
+
def set_postback_url(url)
|
97
|
+
response = client.post "/site/#{id}/", {:actor_token => client.system_token, :postback_url => url}
|
98
|
+
if response.success?
|
99
|
+
properties(true) rescue APIException nil
|
100
|
+
true
|
101
|
+
else
|
102
|
+
raise APIException.new(response.body)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Public: Retrieve a list of owners associated with this site
|
107
|
+
#
|
108
|
+
# Returns [Array<Livefyre::User>] A list of {Livefyre::User users}
|
109
|
+
# Raises: APIException when response is not valid
|
110
|
+
def owners
|
111
|
+
response = client.get "/site/#{id}/owners/", {:actor_token => client.system_token}
|
112
|
+
if response.success?
|
113
|
+
JSON.parse(response.body).map do |u|
|
114
|
+
client.user u.split("@", 2).first
|
115
|
+
end
|
116
|
+
else
|
117
|
+
raise APIException.new(response.body)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Public: Adds a user to the list of owners for this site
|
122
|
+
#
|
123
|
+
# Returns [Bool] true on success
|
124
|
+
# Raises [APIException] when response is not valid
|
125
|
+
def add_owner(user)
|
126
|
+
uid = User.get_user_id(user)
|
127
|
+
response = client.post "/site/#{id}/owners/?actor_token=#{CGI.escape client.system_token}", {:jid => client.jid(uid)}
|
128
|
+
if response.success?
|
129
|
+
true
|
130
|
+
else
|
131
|
+
raise APIException.new(response.body)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Public: Removes a user from the list of owners for this site
|
136
|
+
#
|
137
|
+
# Returns [Bool] true on success
|
138
|
+
# Raises [APIException] when response is not valid
|
139
|
+
def remove_owner(user)
|
140
|
+
user = User.get_user(user, client)
|
141
|
+
response = client.delete "/site/#{id}/owner/#{user.jid}/?actor_token=#{CGI.escape client.system_token}"
|
142
|
+
if response.success?
|
143
|
+
true
|
144
|
+
else
|
145
|
+
raise APIException.new(response.body)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Public: Retrieve a list of owners associated with this site
|
150
|
+
#
|
151
|
+
# Returns [Array<Livefyre::User>] A list of {Livefyre::User users}
|
152
|
+
# Raises: [APIException] when response is not valid
|
153
|
+
def admins
|
154
|
+
response = client.get "/site/#{id}/admins/", {:actor_token => client.system_token}
|
155
|
+
if response.success?
|
156
|
+
JSON.parse(response.body).map do |u|
|
157
|
+
client.user u.split("@", 2).first
|
158
|
+
end
|
159
|
+
else
|
160
|
+
raise APIException.new(response.body)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Public: Adds a user to the list of admins for this site
|
165
|
+
#
|
166
|
+
# Returns [Bool] true on success
|
167
|
+
# Raises [APIException] when response is not valid
|
168
|
+
def add_admin(user)
|
169
|
+
user = User.get_user(user, client)
|
170
|
+
response = client.post "/site/#{id}/admins/?actor_token=#{CGI.escape client.system_token}", {:jid => user.jid}
|
171
|
+
if response.success?
|
172
|
+
true
|
173
|
+
else
|
174
|
+
raise APIException.new(response.body)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Public: Removes a user from the list of admins for this site
|
179
|
+
#
|
180
|
+
# Returns [Bool] true on success
|
181
|
+
# Raises [APIException] when response is not valid
|
182
|
+
def remove_admin(user)
|
183
|
+
user = User.get_user(user, client)
|
184
|
+
response = client.delete "/site/#{id}/admin/#{user.jid}/?actor_token=#{CGI.escape client.system_token}"
|
185
|
+
if response.success?
|
186
|
+
true
|
187
|
+
else
|
188
|
+
raise APIException.new(response.body)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Public: Create a conversation collection on this site
|
193
|
+
#
|
194
|
+
# Returns [Conversation]
|
195
|
+
def create_conversation(article_id, title, link, tags = nil)
|
196
|
+
Conversation.create(client, article_id, title, link, tags)
|
197
|
+
end
|
198
|
+
|
199
|
+
# Internal: Returns a cleaner string representation of this object
|
200
|
+
#
|
201
|
+
# Returns [String] representation of this class
|
202
|
+
def to_s
|
203
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16).rjust(14, "0")} id='#{id}' secret='#{secret}' options=#{options.inspect}>"
|
204
|
+
end
|
205
|
+
|
206
|
+
# Public: Validate a signature as passed by the Livefyre postback service
|
207
|
+
#
|
208
|
+
# params - Hash of request parameters
|
209
|
+
# secret - Site key to validate signature with
|
210
|
+
# time_window - Enforce that the sig_created is within time_window seconds of the current time.
|
211
|
+
# Slush is given to account for system time drift. Pass nil or false to disable timestamp checking.
|
212
|
+
#
|
213
|
+
# Returns [Bool]
|
214
|
+
# Raises [InvalidSignatureException] on failure
|
215
|
+
def self.validate_signature(params, secret, time_window = 300)
|
216
|
+
params = params.clone
|
217
|
+
params.delete :controller
|
218
|
+
params.delete :action
|
219
|
+
sig = (params.delete(:sig) || "").strip
|
220
|
+
raise InvalidSignatureException.new "Missing sig" if sig.nil?
|
221
|
+
raise InvalidSignatureException.new "Missing site key" if secret.nil?
|
222
|
+
|
223
|
+
hash_str = params.sort.map {|v| v.join("=") }.join("&")
|
224
|
+
|
225
|
+
if time_window
|
226
|
+
created_at = params[:sig_created]
|
227
|
+
raise InvalidSignatureException.new "Missing sig_created" if created_at.nil?
|
228
|
+
raise InvalidSignatureException.new "Invalid timestamp" if (Time.now.utc - Time.at(created_at.to_i).utc).abs > time_window
|
229
|
+
end
|
230
|
+
|
231
|
+
check = Base64.encode64 HMAC::SHA1.new(Base64.decode64 secret).update(hash_str).digest
|
232
|
+
check = check.strip
|
233
|
+
raise InvalidSignatureException.new "Invalid signature" if check != sig
|
234
|
+
return sig == check
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module Livefyre
|
2
|
+
# Public: Interface for dealing with Livefyre users by User ID.
|
3
|
+
class User
|
4
|
+
attr_accessor :id, :display_name
|
5
|
+
|
6
|
+
# Public: Create a new Livefyre User proxy.
|
7
|
+
#
|
8
|
+
# id - [String] ID of the user to proxy
|
9
|
+
# client - [Livefyre::Client] an instance of Livefyre::Client. If nil, the default client is used.
|
10
|
+
# display_name - [String] The display name for this user (optional)
|
11
|
+
def initialize(id, client = nil, display_name = nil, args = {})
|
12
|
+
@id = id
|
13
|
+
@client = client || Livefyre.client
|
14
|
+
@display_name = display_name
|
15
|
+
@options = args
|
16
|
+
end
|
17
|
+
|
18
|
+
# Public: Retrieve user information and recent comments for this user from Livefyre
|
19
|
+
#
|
20
|
+
# Returns [Hash] of profile data
|
21
|
+
# Raises [JSON::ParserError] if the returned data cannot be parsed
|
22
|
+
# Raises [APIException] if the API does not return a valid response
|
23
|
+
def profile
|
24
|
+
response = @client.get "/profile/#{id}/", {:actor_token => token}
|
25
|
+
if response.success?
|
26
|
+
begin
|
27
|
+
JSON.parse(response.body)["data"]
|
28
|
+
rescue JSON::ParserError => e
|
29
|
+
raise APIException.new("Parse error: #{e.message}")
|
30
|
+
end
|
31
|
+
else
|
32
|
+
raise APIException.new(result.body)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Public: Setter for the client to associate with this user
|
37
|
+
def client=(client)
|
38
|
+
@client = client
|
39
|
+
end
|
40
|
+
|
41
|
+
# Internal - Fetch an internal Jabber-style ID for this user
|
42
|
+
#
|
43
|
+
# Returns [String] representation of this user
|
44
|
+
def jid
|
45
|
+
"#{id}@#{@client.host}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Public: Creates a signed JWT token for this user
|
49
|
+
#
|
50
|
+
# max_age - [Integer] Expiry time for this token in seconds (default: 86400)
|
51
|
+
#
|
52
|
+
# Returns [String] token
|
53
|
+
def token(max_age = 86400)
|
54
|
+
data = {
|
55
|
+
:domain => @client.host,
|
56
|
+
:user_id => id,
|
57
|
+
:expires => Time.now.to_i + max_age
|
58
|
+
}.tap do |opts|
|
59
|
+
opts[:display_name] = @display_name unless @display_name.nil?
|
60
|
+
end
|
61
|
+
JWT.encode(data, @client.key)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Public: Update this user's profile on Livefyre
|
65
|
+
#
|
66
|
+
# data - [Hash] A hash of user data as defined by the Livefyre user profile schema
|
67
|
+
#
|
68
|
+
# Returns [Bool] true on success
|
69
|
+
# Raises [APIException] if the request failed
|
70
|
+
def push(data)
|
71
|
+
result = @client.post "/profiles/?actor_token=#{CGI.escape @client.system_token}&id=#{id}", {:data => data.to_json}
|
72
|
+
if result.success?
|
73
|
+
true
|
74
|
+
else
|
75
|
+
raise APIException.new(result.body)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Public: Invoke Livefyre ping-to-pull to refresh this user's data
|
80
|
+
#
|
81
|
+
# Returns [Bool] true on success
|
82
|
+
# Raises [APIException] if the request failed
|
83
|
+
def refresh
|
84
|
+
result = @client.post "/api/v3_0/user/#{id}/refresh", {:lftoken => @client.system_token}
|
85
|
+
if result.success?
|
86
|
+
true
|
87
|
+
else
|
88
|
+
raise APIException.new(result.body)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Public: Follow the given conversation
|
93
|
+
#
|
94
|
+
# conversation - [Conversation] to follow
|
95
|
+
# Returns [Boolean] true on success
|
96
|
+
# Raises [APIException] on failure
|
97
|
+
def follow_conversation(conversation)
|
98
|
+
conversation.follow_as self
|
99
|
+
end
|
100
|
+
|
101
|
+
# Public: Unfollow the given conversation
|
102
|
+
#
|
103
|
+
# conversation - [Conversation] to unfollow
|
104
|
+
# Returns [Boolean] true on success
|
105
|
+
# Raises [APIException] on failure
|
106
|
+
def unfollow_conversation(conversation)
|
107
|
+
conversation.unfollow_as self
|
108
|
+
end
|
109
|
+
|
110
|
+
# Public: Coerce a string or [User] into a user ID
|
111
|
+
#
|
112
|
+
# userish - [String/User/Int]A [User] or user ID
|
113
|
+
#
|
114
|
+
# Returns [String] User ID
|
115
|
+
# Raises Exception when value can't be coerced
|
116
|
+
def self.get_user_id(userish)
|
117
|
+
case userish
|
118
|
+
when String
|
119
|
+
userish.split("@", 2).first
|
120
|
+
when Fixnum
|
121
|
+
userish
|
122
|
+
when User
|
123
|
+
userish.id
|
124
|
+
else
|
125
|
+
raise "Invalid user ID"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Public: Convenience method to refresh a user by ID
|
130
|
+
#
|
131
|
+
# id - A Livefyre user ID to refresh
|
132
|
+
#
|
133
|
+
# Returns [Bool] true on success
|
134
|
+
# Raises [APIException] if the request failed
|
135
|
+
def self.refresh(id)
|
136
|
+
new(id).refresh
|
137
|
+
end
|
138
|
+
|
139
|
+
# Public: Fetch a Livefyre::User from a user record or ID
|
140
|
+
#
|
141
|
+
# userish - [String/User/Int] A User or user ID
|
142
|
+
# client - [Livefyre::Client] Client to bind to the User record
|
143
|
+
#
|
144
|
+
# Returns [User]
|
145
|
+
def self.get_user(userish, client)
|
146
|
+
case userish
|
147
|
+
when User
|
148
|
+
userish.client = client
|
149
|
+
userish
|
150
|
+
else
|
151
|
+
new get_user_id(userish), client
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Internal: Returns a cleaner string representation of this object
|
156
|
+
#
|
157
|
+
# Returns [String] representation of this class
|
158
|
+
def to_s
|
159
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16).rjust(14, "0")} id='#{id}' display_name='#{display_name}'>"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|