livefyre-mashable 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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