type_pad_template 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []