blogger 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +8 -0
- data/Manifest.txt +8 -0
- data/README.txt +89 -0
- data/Rakefile +76 -0
- data/lib/blogger.rb +543 -0
- data/lib/google_auth.rb +74 -0
- data/lib/helpers.rb +18 -0
- data/test/test_blogger.rb +8 -0
- metadata +83 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/lib/blogger.rb
ADDED
@@ -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
|
data/lib/google_auth.rb
ADDED
@@ -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
|
data/lib/helpers.rb
ADDED
@@ -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
|
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
|