blogger 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ === 0.5.0 / 2009-03-24
2
+
3
+ * First release
4
+
5
+ * Full access to the Blogger API
6
+ * 6 markup/markdown gems supported
7
+ * punch + pie
8
+
@@ -0,0 +1,8 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/blogger.rb
6
+ lib/google_auth.rb
7
+ lib/helpers.rb
8
+ test/test_blogger.rb
@@ -0,0 +1,89 @@
1
+ = blogger
2
+
3
+ * http://beforefilter.blogspot.com/
4
+
5
+ == DESCRIPTION:
6
+
7
+ The Blogger module provides services related to Blogger, and only blogger. The
8
+ GData gem is great, but it provides a much lower-level interface to Google's
9
+ Blogger API. With the Blogger gem, you have full access to the Blogger API,
10
+ with easy to use classes, and it integrates with 6 different markup/markdown
11
+ gems! What's more, you won't have to muck around with XML.
12
+
13
+ Sure, XML is easy. But why waste time messing around with it? With just 3 or 4
14
+ lines of Blogger.gem code, you'll be able to take a markdown-formatted string
15
+ and post it as a blog post, with categories, and comments.
16
+
17
+ You can also search through all of your comments, old posts, and pretty much
18
+ anything you can do at the blogger.com website, you can do with this gem.
19
+
20
+ == FEATURES/PROBLEMS:
21
+
22
+ * Full implementation of the Blogger API with simple to use classes
23
+ * Support for 6 different markup/markdown gems, some compiled and some pure ruby
24
+ * You'll never touch XML!
25
+ * ETags not fully respected yet, however
26
+
27
+
28
+ == SYNOPSIS:
29
+
30
+ If you know your blog_id, then you can use this code:
31
+
32
+ require 'rubygems'
33
+ require 'blogger'
34
+
35
+ account = Blogger::Account.new("username","password")
36
+ new_post = Blogger::Post.new(:title => "New Post",
37
+ :content => "This is an *awesome* post",
38
+ :formatter => :rdiscount,
39
+ :categories => ["coolness", "awesomeness"])
40
+ account.post(blog_id,post)
41
+
42
+ Otherwise, you'll need your username. You can perform
43
+
44
+ account = Blogger::Account.new(user_id,"username","password")
45
+ new_post = Blogger::Post.new(:title => "New Post",
46
+ :content => "This is an *awesome* post")
47
+ accounts.blogs.first.post(new_post)
48
+
49
+ and after that, throw a comment onto your new post:
50
+
51
+ new_post.comment(:title => "New Comment!",
52
+ :content => "_freaking_ sweet",
53
+ :formatter => :bluecloth)
54
+
55
+ See the docs for different possibilities. The entirety of Google's Blogger API
56
+ is implemented.
57
+
58
+ == REQUIREMENTS:
59
+
60
+ * atom-utils
61
+
62
+ == INSTALL:
63
+
64
+ * sudo gem install blogger --include-dependencies
65
+
66
+ == LICENSE:
67
+
68
+ (The MIT License)
69
+
70
+ Copyright (c) 2009 Michael J. Edgar
71
+
72
+ Permission is hereby granted, free of charge, to any person obtaining
73
+ a copy of this software and associated documentation files (the
74
+ 'Software'), to deal in the Software without restriction, including
75
+ without limitation the rights to use, copy, modify, merge, publish,
76
+ distribute, sublicense, and/or sell copies of the Software, and to
77
+ permit persons to whom the Software is furnished to do so, subject to
78
+ the following conditions:
79
+
80
+ The above copyright notice and this permission notice shall be
81
+ included in all copies or substantial portions of the Software.
82
+
83
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
84
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
85
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
86
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
87
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
88
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
89
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,76 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/blogger.rb'
6
+
7
+ Hoe.new('blogger', Blogger::VERSION) do |p|
8
+ # p.rubyforge_name = 'bloggerx' # if different than lowercase project name
9
+ p.developer('Michael J. Edgar', 'edgar@triqweb.com')
10
+ p.extra_deps << ['atom-tools', '>= 2.0.1']
11
+ p.remote_rdoc_dir = ''
12
+ desc 'Post your blog announcement to blogger.'
13
+ task :post_blogger do
14
+ require 'net/http'
15
+ require 'net/https'
16
+ p.with_config do |config, path|
17
+ break unless config['blogs']
18
+ subject, title, body, urls = p.announcement
19
+ #body += "\n\n#{urls}"
20
+
21
+ config['blogs'].each do |site|
22
+ next unless site['url'] =~ /www\.blogger\.com/
23
+ google_email = site['user']
24
+ google_passwd = site['password']
25
+ source = 'beforefilter.blogspot.com-rubypost'
26
+
27
+ http = Net::HTTP.new('www.google.com', 443)
28
+ http.use_ssl = true
29
+ login_url = '/accounts/ClientLogin'
30
+
31
+ # Setup HTTPS request post data to obtain authentication token.
32
+ data = 'Email=' + google_email +'&Passwd=' + google_passwd + '&source=' + source + '&service=blogger'
33
+ headers = {
34
+ 'Content-Type' => 'application/x-www-form-urlencoded'
35
+ }
36
+
37
+ # Submit HTTPS post request
38
+ resp, data = http.post(login_url, data, headers)
39
+
40
+ unless resp.code.eql? '200'
41
+ puts "Error during authentication, blog at #{site['url']}, ##{site['blog_id']}: #{resp.message}\n"
42
+ else
43
+
44
+ # Parse for the authentication token.
45
+ authToken = data.split("\n").map {|l| l.split("=")}.assoc("Auth")[1]
46
+
47
+ headers = {
48
+ 'Authorization' => 'GoogleLogin auth=' + authToken,
49
+ 'Content-Type' => 'application/atom+xml'
50
+ }
51
+
52
+ data = <<-EOF
53
+ <entry xmlns='http://www.w3.org/2005/Atom'>
54
+ <title type='text'>#{title}</title>
55
+ <content type='xhtml'>
56
+ <div xmlns="http://www.w3.org/1999/xhtml">
57
+ #{body}
58
+ </div>
59
+ </content>
60
+ #{p.blog_categories.inject("") {|acc,cat| acc + "<category scheme=\"http://www.blogger.com/atom/ns#\" term=\"#{cat}\" />\n"}}
61
+ </entry>
62
+ EOF
63
+
64
+ http = Net::HTTP.new('www.blogger.com')
65
+ path = '/feeds/' + site['blog_id'] + '/posts/default'
66
+
67
+ resp, data = http.post(path, data, headers)
68
+ puts "Error while posting, blog at #{site['url']}, ##{site['blog_id']}: #{resp.message}" unless resp.code == 200
69
+ # Expect resp.code == 200 and resp.message == 'OK' for a successful.
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ # vim: syntax=Ruby
@@ -0,0 +1,543 @@
1
+ require File.dirname(__FILE__)+'/google_auth.rb'
2
+ require 'atom/feed'
3
+ require File.dirname(__FILE__)+'/helpers.rb'
4
+ module Blogger
5
+ VERSION = '0.5.0'
6
+ class PostingError < StandardError # :nodoc:
7
+ end
8
+
9
+ # = Formattable
10
+ # This mixin provides a number of formatters to any object with a content method. A large number of
11
+ # formatters are provided to acommodate those who are hosting on servers that limit their gem usage.
12
+ #
13
+ # The available formatters are:
14
+ #
15
+ # [:raw] content is passed directly into the post, with no formatting.
16
+ # [:redcloth] content is parsed as textile (using RedCloth[http://redcloth.org/] gem)
17
+ # [:bluecloth] content is parsed as textile (using BlueCloth[http://www.deveiate.org/projects/BlueCloth] gem)
18
+ # [:rdiscount] content is parsed as markdown (using rdiscount[http://tomayko.com/writings/ruby-markdown-libraries-real-cheap-for-you-two-for-price-of-one] gem)
19
+ # [:peg_markdown] content is parsed as markdown (using peg_markdown[http://tomayko.com/writings/ruby-markdown-libraries-real-cheap-for-you-two-for-price-of-one] gem)
20
+ # [:maruku] content is parsed as markdown (using maruku[http://maruku.rubyforge.org/] gem)
21
+ # [:haml] content is parsed as haml[http://haml.hamptoncatlin.com/]
22
+
23
+ module Formattable
24
+ # Specifies how the content will be formatted. Can be any of the following:
25
+ #
26
+ # [:raw] content is passed directly into the post
27
+ # [:redcloth] content is parsed as textile (using RedCloth gem)
28
+ # [:bluecloth] content is parsed as textile (using BlueCloth gem)
29
+ # [:rdiscount] content is parsed as markdown (using rdiscount gem)
30
+ # [:peg_markdown] content is parsed as markdown (using peg_markdown gem)
31
+ # [:maruku] content is parsed as markdown (using maruku gem)
32
+ # [:haml] content is parsed as haml
33
+ #
34
+ # Note that these aren't all offered for the hell of it - some people have access to
35
+ # compiled gems, and some don't - some don't even get easy access to new
36
+ # pure Ruby gems on their server
37
+ attr_accessor :formatter
38
+
39
+ def format_content #:nodoc:
40
+ send("format_#{@formatter}".to_sym)
41
+ end
42
+
43
+ def format_raw #:nodoc:
44
+ @content
45
+ end
46
+
47
+ def format_redcloth #:nodoc:
48
+ require 'redcloth'
49
+ RedCloth.new(@content).to_html
50
+ end
51
+
52
+ def format_bluecloth #:nodoc:
53
+ require 'bluecloth'
54
+ BlueCloth.new(@content).to_html
55
+ end
56
+
57
+ def format_rdiscount #:nodoc:
58
+ require 'rdiscount'
59
+ RDiscount.new(@content).to_html
60
+ end
61
+
62
+ def format_peg_markdown #:nodoc:
63
+ require 'peg_markdown'
64
+ PEGMarkdown.new(@content).to_html
65
+ end
66
+
67
+ def format_maruku #:nodoc:
68
+ require 'maruku'
69
+ Maruku.new(@content).to_html
70
+ end
71
+
72
+ def format_haml #:nodoc:
73
+ require 'haml'
74
+ Haml::Engine.new(@content).render
75
+ end
76
+
77
+ ACCEPTABLE_FORMATTERS = [:raw, :rdiscount, :redcloth, :bluecloth, :peg_markdown, :maruku, :haml]
78
+
79
+ def formatter=(format) # :nodoc:
80
+ raise ArgumentError.new("Invalid formatter: #{format.inspect}") unless ACCEPTABLE_FORMATTERS.include?(format)
81
+ @formatter = format
82
+ end
83
+ end
84
+
85
+ # = Account
86
+ #
87
+ # The Account class is how you interface with your Blogger.com account. You just
88
+ # need to know your username (google email address), password, and blog ID, and
89
+ # you're good to go.
90
+ #
91
+ # Connect by creating a new Account as such:
92
+ #
93
+ # account = Blogger::Account.new('username','password')
94
+ #
95
+ # You can make sure you're authenticated by calling account.authenticated?
96
+ #
97
+ # Example usage:
98
+ #
99
+ # post = Blogger::Post.new(:title => "Sweet post", :categories = ["awesome", "sweet"])
100
+ # post.draft = true
101
+ # post.content = "I'll fill this in later"
102
+ # Blogger::Account.new('username','password').post(blogid,post)
103
+ #
104
+ class Account
105
+
106
+ attr_accessor :username
107
+ attr_accessor :password
108
+ attr_accessor :auth_token
109
+ attr_accessor :user_id
110
+
111
+ # Returns the blogs in this account. pass +true+ to force a reload.
112
+ def blogs(force_reload=false)
113
+ return @blogs if @blogs && !force_reload
114
+ retrieve_blogs
115
+ end
116
+
117
+ # Returns the blog with the given ID
118
+ def blog_for_id(id)
119
+ blogs.select{|b| b.id.eql? id}.first
120
+ end
121
+
122
+ # Creates a new Account object, and authenticates if the usename and password are
123
+ # provided.
124
+ def initialize(*args)
125
+ @user_id, @username, @password = args[0], "", "" if args.size == 1 && args[0] =~ /^[0-9]+$/
126
+ @username, @password = args[0], args[1] if args.size == 2
127
+ @user_kid, @username, @password = args[0], args[1], args[2] if args.size == 3
128
+ authenticate unless @username.empty? || @password.empty?
129
+ self
130
+ end
131
+
132
+ # Downloads the list of all the user's blogs and stores the relevant information.
133
+ def retrieve_blogs(user_id="")
134
+ NotLoggedInError.new("You aren't logged into Blogger.").raise unless authenticated?
135
+
136
+ user_id = (user_id.empty?) ? @user_id : user_id
137
+
138
+ path = "/feeds/#{user_id}/blogs"
139
+ resp = GoogleAuth.get(path, @auth_token)
140
+ feed = Atom::Feed.parse resp.body
141
+
142
+ @blogs = []
143
+ feed.entries.each do |entry|
144
+ blog = Blogger::Blog.new(:atom => entry, :account => self)
145
+ @blogs << blog
146
+ end
147
+ @blogs
148
+ end
149
+
150
+ # Re-authenticates (or authenticates if we didn't provide the user/pass earlier)
151
+ def authenticate(_username = "", _password = "")
152
+ username = (_username.empty?) ? @username : _username
153
+ password = (_password.empty?) ? @password : _password
154
+
155
+ @auth_token = GoogleAuth::authenticate(username, password)
156
+ @authenticated = !( @auth_token.nil? )
157
+ end
158
+
159
+ # Are we authenticated successfully? This method won't detect timeouts.
160
+ def authenticated?
161
+ @authenticated
162
+ end
163
+
164
+ # Posts the provided post to the blog with the given ID. You can find your blog
165
+ # id by going to your blogger dashboard and selecting your blog - you'll find an
166
+ # address such as this in your bar:
167
+ # http://www.blogger.com/posts.g?blogID=6600774877855692384 <-- your blog id
168
+ #
169
+ # Then just create a Blogger::Post, pass that in as well, and you're done!
170
+ #
171
+ def post(blog_id, post)
172
+ NotLoggedInError.new("You aren't logged into Blogger.").raise unless authenticated?
173
+
174
+ path = "/feeds/#{blog_id}/posts/default"
175
+ data = post.to_s
176
+
177
+ resp = GoogleAuth.post(path, data, @auth_token)
178
+
179
+ raise Blogger::PostingError.new("Error while posting to blog_id #{blog_id}: #{resp.message}") unless resp.code.eql? '201'
180
+ # Expect resp.code == 200 and resp.message == 'OK' for a successful.
181
+ Post.new(:atom => Atom::Entry.parse(resp.body), :blog => blog_for_id(blog_id))
182
+ end
183
+ end
184
+
185
+ # = Blog
186
+ # Encapsulates a Blog retrieved from your user account. This class can be used
187
+ # for searching for posts, or for uploading posts via the +post+ method. Blog
188
+ # objects are only safely retrieved via the Blogger::Account class, either via
189
+ # Account#blogs or Account#blog_for_id.
190
+ #
191
+ class Blog
192
+ attr_accessor :title
193
+ attr_accessor :id
194
+ attr_accessor :authors
195
+ attr_accessor :published
196
+ attr_accessor :updated
197
+ attr_accessor :account
198
+ def initialize(opts = {}) #:nodoc:
199
+ entry = opts[:atom]
200
+ @authors = []
201
+ @title = entry.title.html.strip
202
+ @id = $2 if entry.id =~ /tag:blogger\.com,1999:user\-([0-9]+)\.blog\-([0-9]+)$/
203
+ entry.authors.each do |author|
204
+ @authors << author
205
+ end
206
+ @updated = entry.updated
207
+ @published = entry.published
208
+ @account = opts[:account] if opts[:account]
209
+ end
210
+
211
+ # Uploads the provided post to this blog. Requires that you be logged into your
212
+ # blogger account via the Blogger::Account class.
213
+ def post(post)
214
+ NotLoggedInError.new("You aren't logged into Blogger.").raise unless @account.authenticated?
215
+
216
+ path = "/feeds/#{@id}/posts/default"
217
+ data = post.to_s
218
+
219
+ resp = GoogleAuth.post(path, data, @account.auth_token)
220
+
221
+ raise Blogger::PostingError.new("Error while posting to blog_id #{@id}: #{resp.message}") unless resp.code.eql? '201'
222
+ post.parse Atom::Entry.parse(resp.body)
223
+ end
224
+
225
+ def posts(force_reload = false)
226
+ return @posts if @posts && !(force_reload)
227
+ retrieve_posts
228
+ end
229
+
230
+ # Downloads all the posts for this blog.
231
+ def retrieve_posts
232
+ NotLoggedInError.new("You aren't logged into Blogger.").raise unless @account.authenticated?
233
+
234
+ path = "/feeds/#{@id}/posts/default"
235
+
236
+ resp = GoogleAuth.get(path, @account.auth_token)
237
+
238
+ raise Blogger::RetrievalError.new("Error while retrieving posts for blog id ##{@id}: #{resp.message}") unless resp.code.eql? '200'
239
+ feed = Atom::Feed.parse(resp.body)
240
+
241
+ @posts = []
242
+ feed.entries.each do |entry|
243
+ @posts << Post.new(:atom => entry, :blog => self)
244
+ end
245
+ @posts
246
+ end
247
+ end
248
+
249
+ # = Post
250
+ # The post is the representation of a post on your blogger.com blog. It can handle
251
+ # the title, content, categories, and draft status of the post. These are used for
252
+ # uploading posts (just set the information to your liking) or retrieving them
253
+ # (read from the structure)
254
+ #
255
+ # Example:
256
+ #
257
+ # post = Blogger::Post.new(:title => "Sweet post", :categories = ["awesome", "sweet"])
258
+ # post.draft = true
259
+ # post.content = "I'll fill this in later"
260
+ # Blogger::Account.new('username','password').post(blogid,post)
261
+ #
262
+ class Post
263
+ # the id of the post
264
+ attr_accessor :id #:nodoc:
265
+ # the title of the post
266
+ attr_accessor :title
267
+ # the content of the post
268
+ attr_accessor :content
269
+ # the categories of the post - array of strings
270
+ attr_accessor :categories
271
+ # whether or not the post is a draft
272
+ attr_accessor :draft
273
+ # list of all the comments on this post
274
+ attr_accessor :comments
275
+ # reference to the blog we belong to
276
+ attr_accessor :blog #:nodoc:
277
+ attr_accessor :etag #:nodoc:
278
+
279
+ # Pass in a hash containing pre-set values if you'd like, including
280
+ # * :title - the title of the post
281
+ # * :content - the content of the post, either marked up or not
282
+ # * :categories - a list of categories, or just one string as a category
283
+ # * :draft - boolean, whether the post is a draft or not
284
+ # * :formatter - the formatter to use. :raw, :bluecloth, :redcloth, :peg_markdown, :maruku, :haml or :rdiscount
285
+ # * :blog - the blog this post belongs to
286
+ #
287
+ def initialize(opts = {})
288
+ @categories = []
289
+ if opts[:atom]
290
+ parse opts[:atom]
291
+ else
292
+ opts.each do |key, value|
293
+ next if key =~ /blog/
294
+ instance_variable_set("@#{key}".to_sym, value)
295
+ end
296
+ end
297
+ @blog = opts[:blog]
298
+ @categories = [@categories] unless @categories.is_a? Array
299
+ @formatter = (opts[:formatter]) ? opts[:formatter] : :raw
300
+ end
301
+
302
+ def parse(entry) #:nodoc:
303
+ @atom = entry
304
+ @full_id = entry.id
305
+ @id = $2 if entry.id =~ /^tag:blogger\.com,1999:blog\-([0-9]+)\.post\-([0-9]+)$/
306
+ @title = entry.title.html.strip
307
+ @content = entry.content.html
308
+ @categories = entry.categories.map {|c| c.term}
309
+ @draft = entry.draft?
310
+ @etag = entry.etag
311
+ self
312
+ end
313
+
314
+ # Saves any local changes to the post, and submits them to blogger.
315
+ def save
316
+ NotLoggedInError.new("You aren't logged into Blogger.").raise unless @blog.account.authenticated?
317
+
318
+ update_base_atom(@atom)
319
+ path = "/feeds/#{@blog.id}/posts/default/#{@id}"
320
+
321
+ data = @atom.to_s.clean_atom_junk
322
+
323
+ puts path+"\n\n"
324
+ puts data+"\n\n"
325
+
326
+
327
+ resp = GoogleAuth.put(path,data,@blog.account.auth_token, @etag)
328
+
329
+ raise Blogger::PostingError.new("Error while updating post \"#{@title}\": #{resp.message}") unless resp.code.eql? '200'
330
+
331
+ parse Atom::Entry.parse(resp.body)
332
+ end
333
+ alias_method :push, :save
334
+
335
+ # Deletes the post from your blog.
336
+ def delete
337
+ NotLoggedInError.new("You aren't logged into Blogger.").raise unless @blog.account.authenticated?
338
+
339
+ path = "/feeds/#{@blog.id}/posts/default/#{@id}"
340
+
341
+ resp = GoogleAuth.delete(path,@blog.account.auth_token, @etag)
342
+
343
+ raise Blogger::PostingError.new("Error while deleting post \"#{@title}\": #{resp.message}") unless resp.code.eql? '200'
344
+ @blog.posts.delete self
345
+ self
346
+ end
347
+
348
+ def update_base_atom(entry) #:nodoc:
349
+ entry.title = @title
350
+
351
+ @categories.each do |cat|
352
+ atom_cat = Atom::Category.new
353
+ atom_cat.term = cat
354
+ atom_cat.scheme = 'http://www.blogger.com/atom/ns#'
355
+ entry.categories << atom_cat
356
+ end
357
+
358
+ content = Atom::Content.new(format_content)
359
+ content.type = 'xhtml'
360
+ entry.content = content
361
+ entry.content.type = 'xhtml'
362
+
363
+ entry.draft = @draft
364
+ entry
365
+ end
366
+
367
+
368
+ # Reloads the post from blogger.com, using ETags for efficiency.
369
+ def reload
370
+ NotLoggedInError.new("You aren't logged into Blogger.").raise unless @blog.account.authenticated?
371
+
372
+ path = "/feeds/#{@blog.id}/posts/default/#{@id}"
373
+
374
+ resp = GoogleAuth.get(path, @blog.account.auth_token, @etag)
375
+
376
+ raise Blogger::RetrievalError.new("Error while reloading post \"#{@title}\": #{resp.message}") unless resp.code.eql?('200') || resp.code.eql?('304')
377
+ unless resp.code.eql? '304'
378
+ parse Atom::Entry.parse(resp.body)
379
+ end
380
+ self
381
+ end
382
+
383
+ # Returns whether the post is a draft or not
384
+ def draft?
385
+ @draft
386
+ end
387
+
388
+ # Converts the post to an atom entry in string form. Internally used.
389
+ def to_s
390
+ entry = Atom::Entry.new
391
+ update_base_atom(entry)
392
+
393
+ entry.to_s
394
+ end
395
+
396
+ # Uploads this post to the provided blog.
397
+ def post_to(blog)
398
+ blog.post self
399
+ end
400
+
401
+ include Formattable
402
+
403
+ def inspect #:nodoc:
404
+ {:title => @title, :content => @content, :categories => @categories, :draft => @draft}.to_yaml
405
+ end
406
+
407
+ # Submits a comment to this post. You can use 2 methods of submitting your comment:
408
+ #
409
+ # my_comment = Comment.new(:title => "cool", :content => "I *loved* this post!", :formatter => :rdiscount)
410
+ # mypost.comment(my_comment)
411
+ #
412
+ # or, more easily
413
+ #
414
+ # mypost.comment(:title => "cool", :content => "I *loved* this post!", :formatter => :rdiscount)
415
+ #
416
+ # The currently authenticated user will be the comment author. This is a limitation of Blogger (and
417
+ # probably a good one!)
418
+ def comment(*args)
419
+ comm = (args[0].is_a? Blogger::Comment) ? args[0] : Blogger::Comment.new(args[0])
420
+ comm.post = self
421
+
422
+ NotLoggedInError.new("You aren't logged into Blogger.").raise unless @blog.account.authenticated?
423
+
424
+ path = "/feeds/#{@blog.id}/#{@id}/comments/default"
425
+ data = comm.to_s
426
+
427
+ puts data+"\n\n"
428
+
429
+ resp = GoogleAuth.post(path, data, @blog.account.auth_token)
430
+
431
+ raise Blogger::PostingError.new("Error while commenting to post #{@title}: #{resp.message}") unless resp.code.eql? '201'
432
+ comm.parse Atom::Entry.parse(resp.body)
433
+ end
434
+
435
+ # Returns all comments from the post. Passing +true+ to this method will cause a forced re-download of comments
436
+ def comments(force_download=false)
437
+ return @comments if @comments && !(force_download)
438
+ retrieve_comments
439
+ end
440
+
441
+ # Downloads all comments from the post, and returns them ass Blogger::Comment objects.
442
+ def retrieve_comments
443
+ NotLoggedInError.new("You aren't logged into Blogger.").raise unless @blog.account.authenticated?
444
+
445
+ path = "/feeds/#{@blog.id}/#{@id}/comments/default"
446
+
447
+ resp = GoogleAuth.get(path, @blog.account.auth_token)
448
+
449
+ raise Blogger::RetrievalError.new("Error while retrieving comments for post id ##{@id}: #{resp.message}") unless resp.code.eql? '200'
450
+ feed = Atom::Feed.parse(resp.body)
451
+
452
+ @comments = []
453
+ feed.entries.each do |entry|
454
+ @comments << Comment.new(:atom => entry, :post => self)
455
+ end
456
+ @comments
457
+ end
458
+ end
459
+
460
+ # = Comment
461
+ # Represents a comment on a Blogger blog. Currently, Blogger only supports titles and contents
462
+ # for comments. The currently authenticated user will be used as the poster. To post a comment
463
+ # in response to a blogger post, simply use something like the following:
464
+ #
465
+ # my_comment = Comment.new(:title => "cool", :content => "I *loved* this post!", :formatter => :rdiscount)
466
+ # mypost.comment(my_comment)
467
+ #
468
+ # or, more easily
469
+ #
470
+ # mypost.comment(:title => "cool", :content => "I *loved* this post!", :formatter => :rdiscount)
471
+ #
472
+ class Comment
473
+ # title of the comment
474
+ attr_accessor :title
475
+ # content of the comment, possibly in a markdown/textile format
476
+ attr_accessor :content
477
+ # the blog this comment belongs to (for already posted comments)
478
+ attr_accessor :post
479
+ # the comment's ID (for deletion)
480
+ attr_accessor :id
481
+
482
+ # Creates a new comment. You can pass the following options:
483
+ # * :title - the title of the comment
484
+ # * :content - the content of the comment, either marked up or not
485
+ # * :formatter - the formatter to use. :raw, :bluecloth, :redcloth, :peg_markdown, :maruku, :haml or :rdiscount
486
+ def initialize(opts={})
487
+ if opts[:atom]
488
+ parse(opts[:atom])
489
+ else
490
+ opts.each do |key, value|
491
+ next if key =~ /blog/
492
+ instance_variable_set("@#{key}".to_sym, value)
493
+ end
494
+ end
495
+ @post = opts[:post]
496
+ @formatter = :raw
497
+ end
498
+
499
+ def parse(atom) #:nodoc:
500
+ @id = $2 if atom.id =~ /^tag:blogger\.com,1999:blog\-([0-9]+)\.post\-([0-9]+)$/
501
+ @title = atom.title
502
+ @content = atom.content
503
+ end
504
+
505
+ include Formattable
506
+
507
+ # formats the comment as an atom entry
508
+ def to_s
509
+ entry = Atom::Entry.new
510
+ entry.title = @title
511
+
512
+ content = Atom::Content.new(format_content)
513
+ content.type = 'html'
514
+ entry.content = content
515
+ entry.content.type = 'html'
516
+
517
+ entry.to_s
518
+ end
519
+
520
+ # Deletes the comment from your blog.
521
+ def delete
522
+ NotLoggedInError.new("You aren't logged into Blogger.").raise unless @post.blog.account.authenticated?
523
+
524
+ path = "/feeds/#{@post.blog.id}/#{@post.id}/comments/default/#{@id}"
525
+
526
+ resp = GoogleAuth.delete(path,@post.blog.account.auth_token, @etag)
527
+
528
+ raise Blogger::PostingError.new("Error while deleting comment \"#{@title}\": #{resp.message}") unless resp.code.eql? '200'
529
+ @post.comments.delete self
530
+
531
+ self
532
+ end
533
+
534
+ # Submits the comment to the provided post. Must be an actual post object, not an ID.
535
+ def post_to(post)
536
+ post.comment(self)
537
+ end
538
+
539
+ def inspect #:nodoc:
540
+ {:title => @title, :content => @subject}.to_yaml
541
+ end
542
+ end
543
+ end
@@ -0,0 +1,74 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ class GoogleAuth
4
+
5
+ class AuthenticationFailedError < StandardError; end
6
+ class NotLoggedInError < StandardError; end
7
+
8
+ GA_SOURCE = 'beforefilter.blogspot.com-rubypost'
9
+ GA_GOOGLE = 'www.google.com'
10
+ GA_SERVICE = '/accounts/ClientLogin'
11
+
12
+ def self.authenticate(username, password, print_debug = false)
13
+ http = Net::HTTP.new(GA_GOOGLE, 443)
14
+ http.use_ssl = true
15
+ login_url = GA_SERVICE
16
+
17
+ # Setup HTTPS request post data to obtain authentication token.
18
+ data = 'Email=' + username +'&Passwd=' + password + '&source=' + GA_SOURCE + '&service=blogger'
19
+ headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
20
+
21
+ # Submit HTTPS post request
22
+ response, data = http.post(login_url, data, headers)
23
+ pp response.inspect if print_debug
24
+ pp data.inspect if print_debug
25
+ unless response.code.eql? '200'
26
+ raise AuthenticationFailedError.new("Error during authentication: #{resp.message}")
27
+ else
28
+ data.split("\n").map {|l| l.split("=")}.assoc("Auth")[1]
29
+ end
30
+ end
31
+
32
+ def self.default_headers(token)
33
+ {
34
+ 'Authorization' => 'GoogleLogin auth=' + token,
35
+ 'Content-Type' => 'application/atom+xml',
36
+ 'GData-Version' => '2'
37
+ }
38
+ end
39
+
40
+ def self.get(path, token, etag = nil)
41
+ headers = self.default_headers(token)
42
+ headers.merge!('If-None-Match' => "#{etag}") if etag
43
+
44
+ http = Net::HTTP.new('www.blogger.com')
45
+
46
+ http.get(path, headers)
47
+ end
48
+
49
+
50
+ def self.delete(path, token, etag = nil)
51
+ headers = self.default_headers(token)
52
+
53
+ http = Net::HTTP.new('www.blogger.com')
54
+ req = Net::HTTP::Delete.new(path, headers)
55
+ http.request(req)
56
+ end
57
+
58
+ def self.put(path, data, token, etag = nil)
59
+ headers = self.default_headers(token)
60
+
61
+ http = Net::HTTP.new('www.blogger.com')
62
+ req = Net::HTTP::Put.new(path, headers)
63
+ http.request(req, data)
64
+ end
65
+
66
+ def self.post(path, data, token)
67
+ headers = self.default_headers(token)
68
+
69
+ http = Net::HTTP.new('www.blogger.com')
70
+
71
+ http.post(path, data, headers)
72
+ end
73
+
74
+ end
@@ -0,0 +1,18 @@
1
+ require 'atom/feed'
2
+ class ::String
3
+
4
+ # atom/feed fudges up the atom feed google gives us so we have to manually insert some stuff
5
+ def clean_atom_junk
6
+ str = self
7
+ str = str.sub(/ xmlns/," xmlns:gd='http://schemas.google.com/g/2005' xmlns") unless str =~ /xmlns:gd/
8
+ "<?xml version='1.0' encoding='utf-8'?>" + str.gsub(/ etag='.*?' /," ")
9
+ end
10
+ end
11
+
12
+
13
+ module Atom
14
+ GD = "http://schemas.google.com/g/2005"
15
+ class Entry < Atom::Element
16
+ attrb ["gd", Atom::GD], "etag"
17
+ end
18
+ end
@@ -0,0 +1,8 @@
1
+ require "test/unit"
2
+ require "blogger"
3
+
4
+ class TestBlogger < Test::Unit::TestCase
5
+ def test_sanity
6
+ flunk "write tests or I will kneecap you"
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: blogger
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-25 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: atom-tools
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.0.1
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: hoe
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.11.0
34
+ version:
35
+ description: The Blogger module provides services related to Blogger, and only blogger. The GData gem is great, but it provides a much lower-level interface to Google's Blogger API. With the Blogger gem, you have full access to the Blogger API, with easy to use classes, and it integrates with 6 different markup/markdown gems! What's more, you won't have to muck around with XML. Sure, XML is easy. But why waste time messing around with it? With just 3 or 4 lines of Blogger.gem code, you'll be able to take a markdown-formatted string and post it as a blog post, with categories, and comments. You can also search through all of your comments, old posts, and pretty much anything you can do at the blogger.com website, you can do with this gem.
36
+ email:
37
+ - edgar@triqweb.com
38
+ executables: []
39
+
40
+ extensions: []
41
+
42
+ extra_rdoc_files:
43
+ - History.txt
44
+ - Manifest.txt
45
+ - README.txt
46
+ files:
47
+ - History.txt
48
+ - Manifest.txt
49
+ - README.txt
50
+ - Rakefile
51
+ - lib/blogger.rb
52
+ - lib/google_auth.rb
53
+ - lib/helpers.rb
54
+ - test/test_blogger.rb
55
+ has_rdoc: true
56
+ homepage: http://beforefilter.blogspot.com/
57
+ post_install_message:
58
+ rdoc_options:
59
+ - --main
60
+ - README.txt
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ version:
75
+ requirements: []
76
+
77
+ rubyforge_project: blogger
78
+ rubygems_version: 1.3.1
79
+ signing_key:
80
+ specification_version: 2
81
+ summary: The Blogger module provides services related to Blogger, and only blogger
82
+ test_files:
83
+ - test/test_blogger.rb