livefyre-mashable 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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