type_pad_template 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ TypePad Temlpate
2
+ ================
3
+
4
+ This is gem and command interface to manipulate TypePad advanced templates from command line.
5
+
6
+ Usage
7
+ -----
8
+
9
+ Install gem which provides ``type_pad_template`` command.
10
+
11
+ gem install type_pad_template
12
+
13
+ To see the usage, use help command.
14
+
15
+ type_pad_template help
16
+
17
+ Download and upload templates
18
+ -----------------------------
19
+
20
+ First you need to login to typepad using your email address and password.
21
+
22
+ type_pad_template login -u 'your-email@address'
23
+
24
+ Then list blogs on your account to get a blog id.
25
+
26
+ type_pad_template blogs
27
+
28
+ To download, upload templates, use each command with ``-b`` option.
29
+
30
+ type_pad_template download -b 'your-blog-id'
31
+ type_pad_template upload -b 'your-blog-id'
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rubygems"
4
+ require "type_pad_template"
5
+
6
+ TypePadTemplate::Command.start
@@ -0,0 +1,15 @@
1
+ require "type_pad_template/version"
2
+ require "nokogiri"
3
+ require "typhoeus"
4
+ require "uri"
5
+ require "forwardable"
6
+
7
+ module TypePadTemplate
8
+ autoload :Command, "type_pad_template/command"
9
+ autoload :Form, "type_pad_template/form"
10
+ autoload :Request, "type_pad_template/request"
11
+ autoload :Response, "type_pad_template/response"
12
+ autoload :Account, "type_pad_template/account"
13
+ autoload :Blog, "type_pad_template/blog"
14
+ autoload :Template, "type_pad_template/template"
15
+ end
@@ -0,0 +1,90 @@
1
+ module TypePadTemplate
2
+ class Account
3
+ def self.login(username, password)
4
+ account = new.tap do |a|
5
+ a.login(username, password)
6
+ end
7
+
8
+ account if account.logged_in?
9
+ end
10
+
11
+ attr_reader :login_cookies
12
+
13
+ def initialize(login_cookies = nil)
14
+ @login_cookies = login_cookies
15
+ end
16
+
17
+ def login(username, password)
18
+ form_element = Request.new("/secure/services/signin", :ssl => true).
19
+ dispatch.
20
+ response.
21
+ doc.
22
+ at("//form[@id='signin-form-typepad']")
23
+
24
+ form = Form.new(form_element).tap do |f|
25
+ f[:username] = username
26
+ f[:password] = password
27
+ end
28
+
29
+ @login_cookies = Request.new("/secure/services/signin/save", {
30
+ :ssl => true,
31
+ :method => :post,
32
+ :params => form.to_hash
33
+ }).
34
+ dispatch.
35
+ response.
36
+ cookies
37
+
38
+ self
39
+ end
40
+
41
+ def blogs
42
+ return [] unless logged_in?
43
+
44
+ request("/dashboard") do |response|
45
+ response.
46
+ doc.
47
+ search("//ul[@id='blogs-list']//a[@class='blog-name']").
48
+ map do |element|
49
+ if %r{/([0-9a-f]+)/dashboard$} === element["href"]
50
+ id = $1
51
+ Blog.new(self, id, element.text)
52
+ end
53
+ end.
54
+ compact
55
+ end
56
+ end
57
+
58
+ def logged_in?
59
+ !@login_cookies.empty?
60
+ end
61
+
62
+ def request(path, options = {})
63
+ # FIXME raise a proper exception instead.
64
+ return nil unless logged_in?
65
+
66
+ request = Request.new(path, merge_login_cookie_header(options))
67
+
68
+ if block_given?
69
+ yield request.
70
+ dispatch.
71
+ response
72
+ else
73
+ request
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def merge_login_cookie_header(options)
80
+ options.dup.tap do |options|
81
+ headers = options[:headers] ||= {}
82
+ headers["Cookie"] = [login_cookies_string, headers["Cookie"]].compact.join("; ")
83
+ end
84
+ end
85
+
86
+ def login_cookies_string
87
+ @login_cookies.map{|key, value| "#{key}=#{value}"}.join("; ")
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,50 @@
1
+ module TypePadTemplate
2
+ class Blog
3
+ extend Forwardable
4
+
5
+ def_delegator :account, :request
6
+
7
+ attr_reader :account, :blog_id, :name
8
+
9
+ def initialize(account, blog_id, name)
10
+ @account = account
11
+ @blog_id = blog_id
12
+ @name = name
13
+ end
14
+
15
+ def templates
16
+ return [] unless design_id = current_design_id
17
+
18
+ request("/site/blogs/#{@blog_id}/design/#{design_id}/templates") do |response|
19
+ response.
20
+ doc.
21
+ search("//td[@class='index-templates' or @class='archive-templates' or @class='template-modules']/a[@class='link']").
22
+ map do |element|
23
+ name = element.text
24
+
25
+ output_file_element = element.at("../..//td[@class='output-file']")
26
+
27
+ filename = if output_file_element
28
+ output_file_element.text
29
+ else
30
+ "#{name.downcase.gsub(/[\- ]/, "_")}.template"
31
+ end
32
+
33
+ Template.new(self, name, filename, element["href"])
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def current_design_id
41
+ request("/site/blogs/#{@blog_id}/design") do |response|
42
+ response.
43
+ doc.
44
+ css(".design-current .design-actions a").find do |a|
45
+ %r{/([0-9a-f]+)/templates$} === a["href"]
46
+ end and $1
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,117 @@
1
+ require "thor"
2
+ require "yaml"
3
+ require "highline"
4
+
5
+ module TypePadTemplate
6
+ SESSION_FILE = File.expand_path("~/.type_pad_template_session")
7
+
8
+ class Command < Thor
9
+ def self.blog_id_option
10
+ method_option(:blog_id, {
11
+ :type => :string,
12
+ :aliases => "-b",
13
+ :required => true,
14
+ :desc => "Blog ID which blogs command shows"
15
+ })
16
+ end
17
+
18
+ def self.directory_option
19
+ method_option(:directory, {
20
+ :type => :string,
21
+ :aliases => "-d",
22
+ :desc => "Path to templates directory"
23
+ })
24
+ end
25
+
26
+ def self.pattern_option
27
+ method_option(:pattern, {
28
+ :type => :string,
29
+ :aliases => "-p",
30
+ :desc => "Pattern to select templates"
31
+ })
32
+ end
33
+
34
+ desc "login", "Login to TypePad"
35
+ method_option :username, :type => :string, :aliases => "-u", :desc => "Login email address"
36
+ method_option :password, :type => :string, :aliases => "-p", :desc => "Password"
37
+ def login
38
+ username = options[:username] || highline.ask("Enter login email address: ")
39
+ password = options[:password] || highline.ask("Enter password: "){|q| q.echo = false }
40
+
41
+ account = Account.login(username, password)
42
+
43
+ File.open(SESSION_FILE, "w") do |file|
44
+ YAML.dump(account.login_cookies, file)
45
+ end
46
+ end
47
+
48
+ desc "logout", "Logout from TypePad"
49
+ def logout
50
+ File.unlink(SESSION_FILE)
51
+ end
52
+
53
+ desc "blogs", "List current blogs"
54
+ def blogs
55
+ print_table account.blogs.map{|blog| [blog.blog_id, blog.name]}
56
+ end
57
+
58
+ desc "templates", "List current templates"
59
+ blog_id_option
60
+ def templates
61
+ print_table blog.templates.map{|template| [template.filename, template.name]}
62
+ end
63
+
64
+ desc "download", "Download templates from TypePad"
65
+ blog_id_option
66
+ directory_option
67
+ pattern_option
68
+ def download(filenames = nil)
69
+ pattern_select(blog.templates).each do |template|
70
+ template.enqueue_get do |text|
71
+ say "Downloaded #{template.filename}"
72
+ File.open(File.join(directory, template.filename), "w"){|f| f.write(text)}
73
+ end
74
+ end
75
+ Request.dispatch
76
+ end
77
+
78
+ desc "upload", "Upload templates from local"
79
+ blog_id_option
80
+ directory_option
81
+ pattern_option
82
+ def upload
83
+ pattern_select(blog.templates).each do |template|
84
+ path = File.join(directory, template.filename)
85
+ template.enqueue_post(File.read(path)) do |response|
86
+ say "Uploaded #{template.filename}"
87
+ end
88
+ end
89
+ Request.dispatch
90
+ end
91
+
92
+ private
93
+
94
+ def account
95
+ @account ||= Account.new(YAML.load(File.read(SESSION_FILE)))
96
+ end
97
+
98
+ def blog
99
+ @blog ||= account.blogs.find{|blog| blog.blog_id == options[:blog_id]}
100
+ end
101
+
102
+ def directory
103
+ @directory ||= File.expand_path(options[:directory] || Dir.pwd)
104
+ end
105
+
106
+ def pattern_select(list)
107
+ return list unless options[:pattern]
108
+ list.select do |item|
109
+ File.fnmatch(options[:pattern], item.filename)
110
+ end
111
+ end
112
+
113
+ def highline
114
+ @highline ||= HighLine.new
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,37 @@
1
+ module TypePadTemplate
2
+ class Form
3
+ def initialize(form_element)
4
+ @form_element = form_element
5
+ @values = default_values
6
+ end
7
+
8
+ def [](key)
9
+ @values[key.to_sym]
10
+ end
11
+
12
+ def []=(key, value)
13
+ @values[key.to_sym] = value
14
+ end
15
+
16
+ def to_hash
17
+ @values.dup
18
+ end
19
+
20
+ def method
21
+ @form_element["method"].downcase.to_sym rescue :get
22
+ end
23
+
24
+ def url
25
+ @form_element["action"]
26
+ end
27
+
28
+ private
29
+
30
+ def default_values
31
+ @form_element.search("input[@name!='']").inject({}) do |hash, input|
32
+ hash[input["name"].to_sym] = input["value"] if input["name"]
33
+ hash
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,65 @@
1
+ module TypePadTemplate
2
+ class Request
3
+ DEFAULT_HOST_NAME = "www.typepad.com"
4
+ MAX_CONCURRENCY = 10
5
+
6
+ def self.dispatch
7
+ hydra.run
8
+ end
9
+
10
+ def self.hydra
11
+ @@hydra ||= Typhoeus::Hydra.new(:max_concurrency => MAX_CONCURRENCY)
12
+ end
13
+
14
+ def initialize(url_or_path, options = {})
15
+ @url_or_path = url_or_path
16
+ @options = options
17
+ end
18
+
19
+ def enqueue(&block)
20
+ request.on_complete = lambda do |response|
21
+ block.call(Response.new(response))
22
+ end
23
+ self.class.hydra.queue(request)
24
+ self
25
+ end
26
+
27
+ def dispatch
28
+ hydra = Typhoeus::Hydra.new
29
+ hydra.queue(request)
30
+ hydra.run
31
+ self
32
+ end
33
+
34
+ def response
35
+ @response ||= Response.new(request.response) if request.response
36
+ end
37
+
38
+ private
39
+
40
+ def ssl?
41
+ @options[:ssl]
42
+ end
43
+
44
+ def protocol
45
+ ssl? ? "https" : "http"
46
+ end
47
+
48
+ def url
49
+ @url ||= begin
50
+ uri = URI.parse(@url_or_path)
51
+ unless uri.scheme
52
+ "#{protocol}://#{DEFAULT_HOST_NAME}#{uri.path}"
53
+ else
54
+ uri.to_s
55
+ end
56
+ end
57
+ end
58
+
59
+ def request
60
+ @request ||= Typhoeus::Request.new(url, @options.merge({
61
+ :disable_ssl_peer_verification => ssl?
62
+ }))
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,23 @@
1
+ module TypePadTemplate
2
+ class Response
3
+ def initialize(response)
4
+ @response = response
5
+ end
6
+
7
+ def successful?
8
+ @response.code == 200
9
+ end
10
+
11
+ def cookies
12
+ @cokies ||= Array(@response.headers_hash["Set-Cookie"]).inject({}) do |hash, cookie|
13
+ key, value = cookie.split(/;/, 2).first.split(/=/, 2)
14
+ hash[key] = value
15
+ hash
16
+ end
17
+ end
18
+
19
+ def doc
20
+ @doc ||= Nokogiri::HTML.parse(@response.body)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,77 @@
1
+ module TypePadTemplate
2
+ class Template
3
+ extend Forwardable
4
+
5
+ def_delegator :blog, :request
6
+
7
+ attr_reader :blog, :name, :filename, :url, :text
8
+
9
+ def initialize(blog, name, filename, edit_url)
10
+ @blog = blog
11
+ @name = name
12
+ @filename = filename
13
+ @edit_path = URI.parse(edit_url).path
14
+ end
15
+
16
+ def enqueue_get(&block)
17
+ request(@edit_path).enqueue do |response|
18
+ @text = get_text_from_response(response)
19
+ block.call(text)
20
+ end
21
+ end
22
+
23
+ def get
24
+ request(@edit_path) do |response|
25
+ @text = get_text_from_response(response)
26
+ end
27
+ end
28
+
29
+ def enqueue_post(text, &block)
30
+ request(@edit_path).enqueue do |response|
31
+ form = get_form_for_body(response).tap do |f|
32
+ f[:text] = text
33
+ end
34
+ request_for_form(form).enqueue do |response|
35
+ @text = text
36
+ block.call(response)
37
+ end
38
+ end
39
+ end
40
+
41
+ def post(text)
42
+ request(@edit_path) do |response|
43
+ form = get_form_for_body(response).tap do |f|
44
+ f[:text] = text
45
+ end
46
+
47
+ response = request_for_form(form).dispatch.response
48
+ @text = text
49
+
50
+ response
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def get_form_for_body(response)
57
+ Form.new(response.
58
+ doc.
59
+ at("form[@id='template-form']"))
60
+ end
61
+
62
+ def get_text_from_response(response)
63
+ response.
64
+ doc.
65
+ at("//form[@id='template-form']//textarea[@name='text']").
66
+ text
67
+ end
68
+
69
+ # FIXME make this works in Form.
70
+ def request_for_form(form)
71
+ request(form.url, {
72
+ :method => form.method,
73
+ :params => form.to_hash
74
+ })
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,3 @@
1
+ module TypePadTemplate
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: type_pad_template
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Yoshimasa Niwa
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: typhoeus
16
+ requirement: &70329624589040 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70329624589040
25
+ - !ruby/object:Gem::Dependency
26
+ name: nokogiri
27
+ requirement: &70329624588600 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70329624588600
36
+ - !ruby/object:Gem::Dependency
37
+ name: thor
38
+ requirement: &70329624588060 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70329624588060
47
+ - !ruby/object:Gem::Dependency
48
+ name: highline
49
+ requirement: &70329624587140 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70329624587140
58
+ - !ruby/object:Gem::Dependency
59
+ name: bundler
60
+ requirement: &70329624586680 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70329624586680
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: &70329624586140 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70329624586140
80
+ description: ''
81
+ email:
82
+ - niw@niw.at
83
+ executables:
84
+ - type_pad_template
85
+ extensions: []
86
+ extra_rdoc_files:
87
+ - README.md
88
+ files:
89
+ - bin/type_pad_template
90
+ - lib/type_pad_template.rb
91
+ - lib/type_pad_template/account.rb
92
+ - lib/type_pad_template/blog.rb
93
+ - lib/type_pad_template/command.rb
94
+ - lib/type_pad_template/form.rb
95
+ - lib/type_pad_template/request.rb
96
+ - lib/type_pad_template/response.rb
97
+ - lib/type_pad_template/template.rb
98
+ - lib/type_pad_template/version.rb
99
+ - README.md
100
+ homepage: http://niw.at/
101
+ licenses: []
102
+ post_install_message:
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ! '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ requirements: []
119
+ rubyforge_project: type_pad_template
120
+ rubygems_version: 1.8.11
121
+ signing_key:
122
+ specification_version: 3
123
+ summary: ''
124
+ test_files: []