fmylife 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +9 -0
- data/Manifest.txt +7 -0
- data/README.txt +81 -0
- data/Rakefile +12 -0
- data/lib/fmylife.rb +473 -0
- data/lib/xmlstruct.rb +111 -0
- data/test/test_fmylife.rb +8 -0
- metadata +72 -0
data/History.txt
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
=== 0.5.0 / 2009-03-25
|
2
|
+
|
3
|
+
* Initial release
|
4
|
+
|
5
|
+
* Full access to the API
|
6
|
+
* Read all categories, "top" FMLs, "flop" FMLs, paginated results
|
7
|
+
* Vote on FMLs, moderate pending FMLs
|
8
|
+
* Comment submission and FML submissions aren't working, but this is a result of the API itself being busted
|
9
|
+
* Swap out XML parsers. Choose from REXML, Hpricot, Nokogiri, or libxml-ruby
|
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
= FMyLife
|
2
|
+
|
3
|
+
* http://beforefilter.blogspot.com/
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
This gem allows the user to access the fmylife.com API, which includes
|
8
|
+
reading stories, reading comments, moderating stories that are submitted,
|
9
|
+
submitting stories, submitting comments, and searching for stories.
|
10
|
+
|
11
|
+
In addition, this gem lets you swap in and out which XML parser you use.
|
12
|
+
Since not everyone can take advantage of compiled xml parsers,
|
13
|
+
the typically built-in REXML library is available as well.
|
14
|
+
|
15
|
+
== FEATURES/PROBLEMS:
|
16
|
+
|
17
|
+
* Full access to the fmylife.com API, including reading and submitting
|
18
|
+
stories and submissions
|
19
|
+
* Swap out which XML parser you want to use:
|
20
|
+
* libxml-ruby
|
21
|
+
* hpricot
|
22
|
+
* nokogiri
|
23
|
+
* REXML
|
24
|
+
|
25
|
+
== SYNOPSIS:
|
26
|
+
|
27
|
+
The following code will connect with your API key, authenticate as a user,
|
28
|
+
grab a page of the latest stories and then download all the comments for the
|
29
|
+
very latest story. Then, it will print the names of each comment's author.
|
30
|
+
|
31
|
+
acc = FMyLife::Account.new("yourapikey")
|
32
|
+
acc.authenticate(username,password)
|
33
|
+
latest = acc.latest.first # get the latest story
|
34
|
+
acc.story(latest) # pull comments
|
35
|
+
acc.comments.each do |comm|
|
36
|
+
puts comm.author
|
37
|
+
end
|
38
|
+
|
39
|
+
More interestingly, we should be able to submit stories!
|
40
|
+
|
41
|
+
story = FMyLife::Story.new(:text => "my life sucks. FML",
|
42
|
+
:author => "Anonymous",
|
43
|
+
:category => :love)
|
44
|
+
|
45
|
+
See the individual docs for more examples.
|
46
|
+
|
47
|
+
== REQUIREMENTS:
|
48
|
+
|
49
|
+
* none, though one of the following is recommended:
|
50
|
+
* hpricot
|
51
|
+
* nokogiri
|
52
|
+
* libxml-ruby
|
53
|
+
|
54
|
+
== INSTALL:
|
55
|
+
|
56
|
+
* sudo gem install fmylife
|
57
|
+
|
58
|
+
== LICENSE:
|
59
|
+
|
60
|
+
(The MIT License)
|
61
|
+
|
62
|
+
Copyright (c) 2009 Michael J. Edgar
|
63
|
+
|
64
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
65
|
+
a copy of this software and associated documentation files (the
|
66
|
+
'Software'), to deal in the Software without restriction, including
|
67
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
68
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
69
|
+
permit persons to whom the Software is furnished to do so, subject to
|
70
|
+
the following conditions:
|
71
|
+
|
72
|
+
The above copyright notice and this permission notice shall be
|
73
|
+
included in all copies or substantial portions of the Software.
|
74
|
+
|
75
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
76
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
77
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
78
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
79
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
80
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
81
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/fmylife.rb'
|
6
|
+
|
7
|
+
Hoe.new('fmylife', FMyLife::VERSION) do |p|
|
8
|
+
p.rubyforge_name = 'fmylife' # if different than lowercase project name
|
9
|
+
p.developer('Michael J. Edgar', 'edgar@triqweb.com')
|
10
|
+
end
|
11
|
+
|
12
|
+
# vim: syntax=Ruby
|
data/lib/fmylife.rb
ADDED
@@ -0,0 +1,473 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
require 'digest'
|
4
|
+
require 'time'
|
5
|
+
require File.dirname(__FILE__)+'/xmlstruct.rb'
|
6
|
+
# = FMyLife
|
7
|
+
#
|
8
|
+
# This gem allows the user to access the fmylife.com API, which includes
|
9
|
+
# reading stories, reading comments, moderating stories that are submitted,
|
10
|
+
# submitting stories, submitting comments, and searching for stories.
|
11
|
+
#
|
12
|
+
# In addition, this gem lets you swap in and out which XML parser you use.
|
13
|
+
# Since not everyone can take advantage of compiled xml parsers,
|
14
|
+
# the typically built-in REXML library is available.
|
15
|
+
#
|
16
|
+
# The following code will connect with your API key, authenticate as a user,
|
17
|
+
# grab a page of the latest stories and then download all the comments for the
|
18
|
+
# very latest story. Then, it will print the names of each comment's author.
|
19
|
+
#
|
20
|
+
# acc = FMyLife::Account.new("yourapikey")
|
21
|
+
# acc.authenticate(username,password)
|
22
|
+
# latest = acc.latest.first # get the latest story
|
23
|
+
# acc.story(latest) # pull comments
|
24
|
+
# acc.comments.each do |comm|
|
25
|
+
# puts comm.author
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# More interestingly, we should be able to submit stories!
|
29
|
+
#
|
30
|
+
# story = FMyLife::Story.new(:text => "my life sucks. FML",
|
31
|
+
# :author => "Anonymous",
|
32
|
+
# :category => :love)
|
33
|
+
#
|
34
|
+
# Change parsers as such (you _*MUST*_ pick one before you start accessing the API!)
|
35
|
+
#
|
36
|
+
# FMyLife.parser = :nokogiri
|
37
|
+
# FMyLife.parser = :hpricot
|
38
|
+
# FMyLife.parser = :rexml
|
39
|
+
# FMyLife.parser = :libxml
|
40
|
+
#
|
41
|
+
# See the individual docs for more examples.
|
42
|
+
#
|
43
|
+
module FMyLife
|
44
|
+
VERSION = '0.5.0'
|
45
|
+
|
46
|
+
API_ROOT = 'api.betacie.com'
|
47
|
+
SANDBOX_ROOT = 'sandbox.betacie.com'
|
48
|
+
|
49
|
+
# Categories you are allowed to search and submit with
|
50
|
+
AVAILABLE_CATEGORIES = [ :love , :money , :kids , :work , :health , :sex , :miscellaneous ]
|
51
|
+
|
52
|
+
class AuthenticationError < StandardError; end
|
53
|
+
class RetrievalError < StandardError; end
|
54
|
+
class VotingError < StandardError; end
|
55
|
+
class InvalidCategoryError < ArgumentError; end
|
56
|
+
class VotingTypeError < ArgumentError; end
|
57
|
+
class StoryTooLongError < ArgumentError; end
|
58
|
+
class UnsupportedParserError < ArgumentError; end
|
59
|
+
|
60
|
+
AVAILABLE_PARSERS = [:nokogiri, :hpricot, :rexml, :libxml]
|
61
|
+
def self.parser=(val)
|
62
|
+
raise UnsupportedParserError.new("An unsupported parser was provided: #{val}") unless AVAILABLE_PARSERS.include? val
|
63
|
+
@@parser = val
|
64
|
+
case @@parser
|
65
|
+
when :nokogiri
|
66
|
+
require 'nokogiri'
|
67
|
+
when :hpricot
|
68
|
+
require 'hpricot'
|
69
|
+
when :rexml
|
70
|
+
require 'rexml/document'
|
71
|
+
when :libxml
|
72
|
+
require 'libxml'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
def self.parser
|
76
|
+
@@parser
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# = Account
|
81
|
+
# An account/connection to FMyLife.com. This class provides access to all of the
|
82
|
+
# features of the FML api.
|
83
|
+
#
|
84
|
+
class Account
|
85
|
+
include CanParse
|
86
|
+
|
87
|
+
attr_accessor :auth_token
|
88
|
+
attr_accessor :api_key
|
89
|
+
attr_accessor :sandbox_mode
|
90
|
+
|
91
|
+
# Creates an FMyLife with the given developer key. The language can also be 'fr'
|
92
|
+
# for French. Sandbox mode defaults to being off.
|
93
|
+
def initialize(key = 'readonly', language='en')
|
94
|
+
@api_key, @language = key, language
|
95
|
+
@sandbox_mode = false
|
96
|
+
end
|
97
|
+
|
98
|
+
# Logs the connection in with the provided username and password
|
99
|
+
def authenticate(username, password)
|
100
|
+
path = "/account/login/#{username}/#{Digest::MD5.hexdigest(password)}"
|
101
|
+
doc = http_post(path, '')
|
102
|
+
|
103
|
+
case code(doc)
|
104
|
+
when 0
|
105
|
+
raise FMyLife::AuthenticationError.new("Error logging into FMyLife: #{error(doc)}")
|
106
|
+
when 1
|
107
|
+
@auth_token = xml_content xpath(doc,'//token').first
|
108
|
+
end
|
109
|
+
self
|
110
|
+
end
|
111
|
+
|
112
|
+
# Logs the user out of fmylife.com.
|
113
|
+
def logout
|
114
|
+
raise FMyLife::AuthenticationError.new("You aren't logged in.") if @auth_token.nil?
|
115
|
+
path = "/account/logout/#{@auth_token}"
|
116
|
+
doc = http_get(path)
|
117
|
+
case code(doc)
|
118
|
+
when 0
|
119
|
+
raise FMyLife::AuthenticationError.new("Error logging out: #{error(doc)}")
|
120
|
+
when 1
|
121
|
+
@auth_token = nil
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Returns the most recent stories, paginated (starting at 0). Maximum return of 15 stories.
|
126
|
+
def latest(page=0)
|
127
|
+
path = "/view/last/#{page}"
|
128
|
+
retrieve_stories(path)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns a page of the top stories. interval can be "day", "week", or "month", to change
|
132
|
+
# how far back to look. Paginated to 15 entries, and +page+ starts at 0.
|
133
|
+
def top(interval = nil, page=0)
|
134
|
+
param = case interval
|
135
|
+
when nil
|
136
|
+
"top"
|
137
|
+
when :day
|
138
|
+
"top_day"
|
139
|
+
when :week
|
140
|
+
"top_week"
|
141
|
+
when :month
|
142
|
+
"top_month"
|
143
|
+
else
|
144
|
+
raise ArgumentError.new("Invalid top-fml interval: #{interval}")
|
145
|
+
end
|
146
|
+
path = "/view/#{param}/#{page}"
|
147
|
+
retrieve_stories(path)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns a page of the "flop" (lowest rated) stories. interval can be "day", "week", or "month",
|
151
|
+
# to change how far back to look. Paginated to 15 entries, and +page+ starts at 0.
|
152
|
+
def flop(interval = nil, page=0)
|
153
|
+
param = case interval.to_sym
|
154
|
+
when nil
|
155
|
+
"flop"
|
156
|
+
when :day
|
157
|
+
"flop_day"
|
158
|
+
when :week
|
159
|
+
"flop_week"
|
160
|
+
when :month
|
161
|
+
"flop_month"
|
162
|
+
else
|
163
|
+
raise ArgumentError.new("Invalid flop-fml interval: #{interval}")
|
164
|
+
end
|
165
|
+
path = "/view/#{param}/#{page}"
|
166
|
+
retrieve_stories(path)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns a page of stories with the provided category. You may abbreviate "miscellaneous" as "misc".
|
170
|
+
def category(category, page=0)
|
171
|
+
category = :miscellaneous if category.to_s =~ /misc/ # poor spellers unite!
|
172
|
+
unless FMyLife::AVAILABLE_CATEGORIES.include?(category.to_sym)
|
173
|
+
raise InvalidCategoryError.new("You provided an invalid category: #{category}")
|
174
|
+
end
|
175
|
+
path = "/view/#{category}/#{page}"
|
176
|
+
retrieve_stories(path)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Gets a random FML story, with or without comments.
|
180
|
+
def random(get_comments = true)
|
181
|
+
path = "/view/random"
|
182
|
+
retrieve_story(path,get_comments)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Gets a specific +story+, with or without comments. The +_story_+ parameter may be either a
|
186
|
+
# FMyLife::Story object, or an ID number.
|
187
|
+
def story(_story, get_comments = true)
|
188
|
+
number = (_story.is_a? FMyLife::Story) ? _story.id : _story
|
189
|
+
path = "/view/#{number}"
|
190
|
+
path = path + "/nocomment" unless get_comments
|
191
|
+
retrieve_story(path,get_comments)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Gets the stories that the currently logged-in user has not seen.
|
195
|
+
def new
|
196
|
+
path = "/view/new"
|
197
|
+
retrieve_stories(path,true)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Gets the currently logged-in user's favorite stories.
|
201
|
+
def favorites
|
202
|
+
path = "/view/favorites"
|
203
|
+
retrieve_stories(path,true)
|
204
|
+
end
|
205
|
+
|
206
|
+
def search(term)
|
207
|
+
path = "/view/search/"
|
208
|
+
retrieve_stories(path,false,{:search => term})
|
209
|
+
end
|
210
|
+
|
211
|
+
# Helper method for retrieving a single story with comments. Internal use only.
|
212
|
+
def retrieve_story(path, get_comments = true)
|
213
|
+
doc = http_get(path)
|
214
|
+
|
215
|
+
case code(doc)
|
216
|
+
when 0
|
217
|
+
raise FMyLife::RetrievalError.new("Error retrieving story ##{number}: #{error(doc)}")
|
218
|
+
when 1
|
219
|
+
story = FMyLife::Story.new(:xml => xpath(doc,'//items/item').first)
|
220
|
+
if get_comments
|
221
|
+
comments = []
|
222
|
+
xpath(doc,'//comments/comment').each do |entry|
|
223
|
+
comments << FMyLife::Comment.new(:xml => entry)
|
224
|
+
end
|
225
|
+
story.comments = comments
|
226
|
+
end
|
227
|
+
story
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Submits a Story to fmylife.com. You must be logged in (with +authenticate+). The +story+ parameter
|
232
|
+
# can be either an FMyLife::Story, or a hash with the following values:
|
233
|
+
#
|
234
|
+
# [:author] The name of the story's author
|
235
|
+
# [:text] The text of the story. Maximum 300 characters.
|
236
|
+
# [:cat] The category to use. Must be one of AVAILABLE_CATEGORIES.
|
237
|
+
#
|
238
|
+
def submit(story)
|
239
|
+
raise FMyLife::AuthenticationError.new("You aren't logged in.") if @auth_token.nil?
|
240
|
+
params = (story.is_a? FMyLife::Story) ? {:author => story.author, :text => story.text, :cat => story.category} : story
|
241
|
+
params[:cat] = params.delete :category if params[:category]
|
242
|
+
raise FMyLife::StoryTooLongError.new("Stories can be no longer than 300 characters.") if params[:text].size > 300
|
243
|
+
params[:token] = @auth_token
|
244
|
+
path = "/submit"
|
245
|
+
doc = http_get(path, params)
|
246
|
+
case code(doc)
|
247
|
+
when 0
|
248
|
+
raise VotingError.new("Error while submitting story with text \"#{params[:text]}\": #{error(doc)}")
|
249
|
+
when 1
|
250
|
+
true
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# Votes for a story. +type+ must be :agree or :deserved . Story can be either a FMyLife::Story or an ID number.
|
255
|
+
def vote(story, type)
|
256
|
+
raise VotingTypeError.new("Your vote must be either 'agree' or 'deserved'.") unless [:agree, :deserved].include? type.to_sym
|
257
|
+
id = (story.is_a? FMyLife::Story) ? story.id : story
|
258
|
+
path = "/vote/#{id}/#{type}"
|
259
|
+
doc = http_get(path)
|
260
|
+
case code(doc)
|
261
|
+
when 0
|
262
|
+
raise VotingError.new("Error while voting on story ##{story.id}: #{error(doc)}")
|
263
|
+
when 1
|
264
|
+
true
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Submits a Comment to fmylife.com. You must be logged in (with +authenticate+). The +story+ parameter
|
269
|
+
# can be either an FMyLife::Story, or an ID number. +arg+ can be an FMyLife::Comment or a hash with
|
270
|
+
# the following values:
|
271
|
+
#
|
272
|
+
# [:text] The text of the story. Maximum 300 characters.
|
273
|
+
# [:url] The url for the currently logged-in user.
|
274
|
+
#
|
275
|
+
def comment(story, arg)
|
276
|
+
raise FMyLife::AuthenticationError.new("You aren't logged in.") if @auth_token.nil?
|
277
|
+
params = (arg.is_a? FMyLife::Comment) ? {:text => arg.text, :url => arg.author_url} : arg
|
278
|
+
params[:id] = (story.is_a? FMyLife::Story) ? story.id : story
|
279
|
+
params[:token] = @auth_token
|
280
|
+
path = "/comment"
|
281
|
+
doc = http_get(path, params)
|
282
|
+
case code(doc)
|
283
|
+
when 0
|
284
|
+
raise VotingError.new("Error while voting on story ##{params[:id]}: #{error(doc)}")
|
285
|
+
when 1
|
286
|
+
true
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Returns an FMyLife::Developer object with information on the current developer (determined
|
291
|
+
# by the id provided when the FMyLife was instantiated)
|
292
|
+
def developer_info
|
293
|
+
path = "/dev"
|
294
|
+
doc = http_get(path)
|
295
|
+
case code(doc)
|
296
|
+
when 0
|
297
|
+
raise RetrievalError.new("Error retrieving developer info: #{error(doc)}")
|
298
|
+
when 1
|
299
|
+
Developer.new(:xml => doc)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Returns all unmoderated stories.
|
304
|
+
def all_unmoderated
|
305
|
+
path = "/mod/view"
|
306
|
+
doc = http_get(path)
|
307
|
+
results = []
|
308
|
+
xpath(doc,"//items/item").each do |item|
|
309
|
+
results << xml_content(item).to_i
|
310
|
+
end
|
311
|
+
results
|
312
|
+
end
|
313
|
+
|
314
|
+
# Returns a single unmoderated story, based on its ID number.
|
315
|
+
def unmoderated(id)
|
316
|
+
path = "/mod/view/#{id}"
|
317
|
+
retrieve_stories(path, true)
|
318
|
+
end
|
319
|
+
|
320
|
+
# Returns the last story to be moderated.
|
321
|
+
def last_moderated
|
322
|
+
path = "/mod/last"
|
323
|
+
retrieve_stories(path, true)
|
324
|
+
end
|
325
|
+
|
326
|
+
# Moderates a given story with a "yes" or "no" answer. ID can be either an FMyLife::Story object
|
327
|
+
# or an ID number. +type+ should be :yes or :no .
|
328
|
+
def moderate(story, type)
|
329
|
+
raise VotingTypeError.new("Your vote must be either 'yes' or 'no'.") unless [:yes, :no].include? type.to_sym
|
330
|
+
id = (story.is_a? Story) ? story.id : story
|
331
|
+
path = "/mod/#{type}/#{id}"
|
332
|
+
doc = http_get(path, :token => @auth_token)
|
333
|
+
case code(doc)
|
334
|
+
when 0
|
335
|
+
raise VotingError.new("Error while moderating story ##{story.id}: #{error(doc)}")
|
336
|
+
when 1
|
337
|
+
true
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# Helper method for retreiving multiple stories, without comments. Internal Use Only.
|
342
|
+
def retrieve_stories(path, pass_token = false, params = {})
|
343
|
+
params.merge!({:token => @auth_token}) if pass_token
|
344
|
+
doc = http_get(path, params)
|
345
|
+
case code(doc)
|
346
|
+
when 0
|
347
|
+
raise FMyLife::RetrievalError.new("Error retrieving stories: #{error(doc)}")
|
348
|
+
when 1
|
349
|
+
stories = []
|
350
|
+
xpath(doc,'//items/item').each do |entry|
|
351
|
+
story = FMyLife::Story.new(:xml => entry)
|
352
|
+
stories << story
|
353
|
+
end
|
354
|
+
stories
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
# Helper method for retrieving URLs via GET while escaping parameters and including API-specific
|
359
|
+
# parameters
|
360
|
+
def http_get(path, query_params = {})
|
361
|
+
query_params.merge!(:key => @api_key, :language => @language)
|
362
|
+
http = Net::HTTP.new((@sandbox_mode) ? SANDBOX_ROOT : API_ROOT)
|
363
|
+
path = path + "?" + URI.escape(query_params.map {|k,v| "#{k}=#{v}"}.join("&"), /[^-_!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]/n)
|
364
|
+
puts path
|
365
|
+
resp = http.get(path)
|
366
|
+
xml_doc(resp.body)
|
367
|
+
end
|
368
|
+
|
369
|
+
# Helper method for retrieving URLs via POST while escaping parameters and including API-specific
|
370
|
+
# parameters
|
371
|
+
def http_post(path, data, query_params = {})
|
372
|
+
query_params.merge!(:key => @api_key, :language => @language)
|
373
|
+
http = Net::HTTP.new((@sandbox_mode) ? SANDBOX_ROOT : API_ROOT)
|
374
|
+
path = path + "?" + URI.escape(query_params.map {|k,v| "#{k}=#{v}"}.join("&"), /[^-_!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]/n)
|
375
|
+
resp = http.post(path, data)
|
376
|
+
xml_doc(resp.body)
|
377
|
+
end
|
378
|
+
|
379
|
+
|
380
|
+
|
381
|
+
# Helper method that finds the error code from an XML document from fmylife
|
382
|
+
def code(doc)
|
383
|
+
xml_content(xpath(doc,'//code').first).to_i
|
384
|
+
end
|
385
|
+
|
386
|
+
# Helper method that finds the error string from an XML document from fmylife
|
387
|
+
def error(doc)
|
388
|
+
puts doc.inspect
|
389
|
+
begin
|
390
|
+
xml_content xpath(doc,'//error').first
|
391
|
+
rescue
|
392
|
+
xml_content xpath(doc,'//errors').first
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
# = Story
|
398
|
+
# Encapsulates a story on fmylife.com. These are primary used for reading stories
|
399
|
+
# off the site, but it can be used to submit stories as well. An example:
|
400
|
+
#
|
401
|
+
# fml = FMyLife.new("apikeyhere")
|
402
|
+
# story = FMyLife::Story.new(:author => "Anonymous",
|
403
|
+
# :category => :love,
|
404
|
+
# :text => "blah blah blah. FML")
|
405
|
+
# fml.submit(story)
|
406
|
+
#
|
407
|
+
# To read one, simple use it's variables:
|
408
|
+
# story = fml.latest.first
|
409
|
+
# author = story.author
|
410
|
+
# text = story.text
|
411
|
+
#
|
412
|
+
class Story < XMLStruct
|
413
|
+
attr_accessor :comments
|
414
|
+
|
415
|
+
field :id, :attribute, "id", "."
|
416
|
+
field :author, :node, "author"
|
417
|
+
field :category, :node, "category"
|
418
|
+
field :date, :proc, lambda { |entry| Time.xmlschema(xml_content(xpath(entry,"date").first)) }
|
419
|
+
field :agreed, :node, "agree"
|
420
|
+
field :deserved, :node, "deserved"
|
421
|
+
field :num_comments, :node, "comments"
|
422
|
+
field :text, :node, "text"
|
423
|
+
field :comments_flag, :node, "comments_flag"
|
424
|
+
|
425
|
+
# Initialize a new story. Available options are:
|
426
|
+
#
|
427
|
+
# [:author] The name of the author
|
428
|
+
# [:category] The category (must be from FMyLife::AVAILABLE_CATEGORIES)
|
429
|
+
# [:text] The text of the submissions
|
430
|
+
def initialize(opts = {}); super(opts); end
|
431
|
+
end
|
432
|
+
|
433
|
+
# = Comment
|
434
|
+
# Encapsulates a comment on fmylife.com. Belongs to a story. This class
|
435
|
+
# can be used for submitting a comment, or for reading them. To submit one,
|
436
|
+
# initialize them as follows:
|
437
|
+
#
|
438
|
+
# comment = FMyLife::Comment.new(:text => "good story")
|
439
|
+
# fml.comment(story, comment)
|
440
|
+
#
|
441
|
+
class Comment < XMLStruct
|
442
|
+
field :id, :attribute, "id", "."
|
443
|
+
field :order, :attribute, "pub_id", "."
|
444
|
+
field :staff, :proc, Proc.new {|entry| (xml_attribute(entry,"staff").to_i == 1)}
|
445
|
+
field :author, :node, "author"
|
446
|
+
field :author_url, :attribute, "url", "author"
|
447
|
+
field :date, :proc, lambda {|entry| Time.xmlschema(xml_content(xpath(entry,"date").first))}
|
448
|
+
field :text, :node, "text"
|
449
|
+
|
450
|
+
# Initialize a new comment. Available options are:
|
451
|
+
#
|
452
|
+
# [:text] The text of the submission
|
453
|
+
#
|
454
|
+
# All others will have no effect upon submitting the comment.
|
455
|
+
def initialize(opts={}); super(opts); end
|
456
|
+
end
|
457
|
+
|
458
|
+
# = Developer
|
459
|
+
#
|
460
|
+
# This class holds the developer's information that one can request using the
|
461
|
+
# FMyLife::Account#developer_info method. All information is read-only.
|
462
|
+
#
|
463
|
+
class Developer < XMLStruct
|
464
|
+
field :name, :node, "//infos/name"
|
465
|
+
field :project, :node, "//infos/project"
|
466
|
+
field :description, :node, "//infos/description"
|
467
|
+
field :url, :node, "//infos/url"
|
468
|
+
field :email, :node, "//infos/mail"
|
469
|
+
field :last24h, :node, "//actions/last24h"
|
470
|
+
field :alltime, :node, "//actions/alltime"
|
471
|
+
field :tokens, :node, "//tokens"
|
472
|
+
end
|
473
|
+
end
|
data/lib/xmlstruct.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
# Methods for parser-agnostic parsing
|
2
|
+
module CanParse
|
3
|
+
# Helper method that encapsulates a string into an XML document
|
4
|
+
def xml_doc(body)
|
5
|
+
case FMyLife.parser
|
6
|
+
when :nokogiri
|
7
|
+
Nokogiri::XML(body)
|
8
|
+
when :hpricot
|
9
|
+
Hpricot(body)
|
10
|
+
when :rexml
|
11
|
+
REXML::Document.new(body)
|
12
|
+
when :libxml
|
13
|
+
LibXML::XML::Parser.string(body).parse
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def xpath(element,path)
|
18
|
+
case FMyLife.parser
|
19
|
+
when :nokogiri
|
20
|
+
element.xpath(path)
|
21
|
+
when :hpricot
|
22
|
+
puts "in hpricot"
|
23
|
+
element/path #force the //
|
24
|
+
when :rexml
|
25
|
+
REXML::XPath.match(element,path)
|
26
|
+
when :libxml
|
27
|
+
element.find(path)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def xml_content(element)
|
32
|
+
case FMyLife.parser
|
33
|
+
when :nokogiri
|
34
|
+
element.content
|
35
|
+
when :hpricot
|
36
|
+
element.inner_text
|
37
|
+
when :rexml
|
38
|
+
element.text
|
39
|
+
when :libxml
|
40
|
+
element.content
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def xml_attribute(element,attribute)
|
45
|
+
case FMyLife.parser
|
46
|
+
when :nokogiri
|
47
|
+
element[attribute]
|
48
|
+
when :hpricot
|
49
|
+
element.get_attribute(attribute)
|
50
|
+
when :rexml
|
51
|
+
element.attributes[attribute]
|
52
|
+
when :nokogiri
|
53
|
+
element.attributes[attribute]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Generic XML Structure
|
59
|
+
class XMLStruct
|
60
|
+
|
61
|
+
include CanParse
|
62
|
+
extend CanParse
|
63
|
+
|
64
|
+
class << self
|
65
|
+
|
66
|
+
attr_accessor :fields
|
67
|
+
|
68
|
+
def field(name, type, getter, path="")
|
69
|
+
@fields ||= {}
|
70
|
+
if type == :attribute
|
71
|
+
@fields[name.to_sym] = {:type => type, :getter => getter, :path => path}
|
72
|
+
else
|
73
|
+
@fields[name.to_sym] = {:type => type, :getter => getter}
|
74
|
+
end
|
75
|
+
attr_accessor name
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def post_initialize; end
|
81
|
+
|
82
|
+
def initialize(opts = {})
|
83
|
+
if opts[:xml]
|
84
|
+
parse_xml opts[:xml]
|
85
|
+
else
|
86
|
+
opts.each do |key, value|
|
87
|
+
instance_variable_set("@#{key}".to_sym, value)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
post_initialize
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
def parse_xml(entry)
|
95
|
+
myfields = self.class.fields
|
96
|
+
myfields.each do |k,v|
|
97
|
+
if v[:getter].is_a?(String) || v[:getter].is_a?(Symbol)
|
98
|
+
if v[:type] == :attribute
|
99
|
+
instance_variable_set("@#{k}".to_sym, xml_attribute(xpath(entry,v[:path]).first, v[:getter].to_s))
|
100
|
+
elsif v[:type] == :node
|
101
|
+
node = xpath(entry, v[:getter].to_s).first
|
102
|
+
if node
|
103
|
+
instance_variable_set "@#{k}".to_sym, xml_content(node)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
elsif v[:getter].is_a?(Proc)
|
107
|
+
instance_variable_set "@#{k}".to_sym, v[:getter].call(entry)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fmylife
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael J. Edgar
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-03-27 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: hoe
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.11.0
|
24
|
+
version:
|
25
|
+
description: This gem allows the user to access the fmylife.com API, which includes reading stories, reading comments, moderating stories that are submitted, submitting stories, submitting comments, and searching for stories. In addition, this gem lets you swap in and out which XML parser you use. Since not everyone can take advantage of compiled xml parsers, the typically built-in REXML library is available as well.
|
26
|
+
email:
|
27
|
+
- edgar@triqweb.com
|
28
|
+
executables: []
|
29
|
+
|
30
|
+
extensions: []
|
31
|
+
|
32
|
+
extra_rdoc_files:
|
33
|
+
- History.txt
|
34
|
+
- Manifest.txt
|
35
|
+
- README.txt
|
36
|
+
files:
|
37
|
+
- History.txt
|
38
|
+
- Manifest.txt
|
39
|
+
- README.txt
|
40
|
+
- Rakefile
|
41
|
+
- lib/fmylife.rb
|
42
|
+
- lib/xmlstruct.rb
|
43
|
+
- test/test_fmylife.rb
|
44
|
+
has_rdoc: true
|
45
|
+
homepage: http://beforefilter.blogspot.com/
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options:
|
48
|
+
- --main
|
49
|
+
- README.txt
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "0"
|
57
|
+
version:
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: "0"
|
63
|
+
version:
|
64
|
+
requirements: []
|
65
|
+
|
66
|
+
rubyforge_project: fmylife
|
67
|
+
rubygems_version: 1.3.1
|
68
|
+
signing_key:
|
69
|
+
specification_version: 2
|
70
|
+
summary: This gem allows the user to access the fmylife.com API, which includes reading stories, reading comments, moderating stories that are submitted, submitting stories, submitting comments, and searching for stories
|
71
|
+
test_files:
|
72
|
+
- test/test_fmylife.rb
|