nippocf 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 579949b4522549a1a6035a829c03bef5577e0269
4
+ data.tar.gz: 2a6c9671173d25b5b3385cc8e4a8458935783467
5
+ SHA512:
6
+ metadata.gz: 1d237fc674ff329c45eb069d1e122d8bdeaf3a260579947dd26108f5fff8d98e871c7019c2076f2f5ac6d96709a99629f1c6429fed3da812e4028426caeeaebe
7
+ data.tar.gz: 72a937c571bdf1d66fa5158f94070d8a67ea823c79a048d95b5f5fb1f0ce7ff042b6ad86ea9122c90861be992ac86a5a907eb3925f92642bd77a2cb169372a37
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .env
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in nippocf.gemspec
4
+ gemspec
5
+
6
+ gem 'dotenv'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Ryota Arai
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # Nippocf
2
+
3
+ Write nippo (daily report in Japanese) on Atlassian Confluence.
4
+
5
+ Nippocf:
6
+ * posts blog entry
7
+ * convert markdown to html
8
+
9
+ ## Installation
10
+
11
+ $ gem install nippocf
12
+
13
+ ## Requirements
14
+
15
+ * Mac OS X
16
+ * because this gem uses keychain to save your password
17
+
18
+ ## Usage
19
+
20
+ ```
21
+ $ export CONFL_URL="https://your-confluence-host"
22
+ $ export CONFL_USERNAME="your.user.name"
23
+ $ nippocf
24
+ Password for your.user.name: <type here, the password will be stored in keychain>
25
+ # Editor opens automatically
26
+ # Write nippo in markdown
27
+ # Nippocf posts a blog entry.
28
+ ```
29
+
30
+ ```
31
+ $ # For instance, it's 2013/12/31
32
+ $ # Edit nippo of 2013/12/31
33
+ $ nippocf
34
+ $ # Edit nippo of 2013/12/20
35
+ $ nippocf 20
36
+ $ # Edit nippo of 2013/11/20
37
+ $ nippocf 11/20
38
+ $ # Edit nippo of 2012/11/20
39
+ $ nippocf 2012/11/20
40
+ ```
41
+
42
+ ### SOCKS Proxy
43
+
44
+ If you set `ENV['SOCKS_PROXY']`, this connect to confluence server via SOCKS proxy.
45
+
46
+ ## Contributing
47
+
48
+ 1. Fork it ( http://github.com/<my-github-username>/nippo_confl/fork )
49
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
50
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
51
+ 4. Push to the branch (`git push origin my-new-feature`)
52
+ 5. Create new Pull Request
53
+
54
+ # Licence
55
+
56
+ MIT Licence
57
+
58
+ This includes source code of confluence4r which requires MIT licence.
59
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/nippocf ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'nippocf/cli'
4
+ Nippocf::CLI.new(ARGV).run
5
+
@@ -0,0 +1,63 @@
1
+ require 'yaml'
2
+
3
+ require 'confluence/confluence_rpc'
4
+
5
+ class Confluence::Connector
6
+
7
+ attr_accessor :username, :password, :default_service, :url,
8
+ :admin_proxy_username, :admin_proxy_password
9
+
10
+ def initialize(options = {})
11
+ load_confluence_config
12
+ @url ||= options[:url]
13
+ @username = options[:username] || @username
14
+ @password = options[:password] || @password
15
+ @default_service = options[:service] || 'confluence1'
16
+ end
17
+
18
+ def connect(service = nil)
19
+ unless url and username and password and service || default_service
20
+ raise "Cannot get Confluence::RPC instance because the confluence url, username, password, or service have not been set"
21
+ end
22
+
23
+ rpc = Confluence::RPC.new(url, service || default_service)
24
+ rpc.login(username, password)
25
+
26
+ return rpc
27
+ end
28
+
29
+ def load_confluence_config
30
+ conf_path = File.expand_path("~/.confluence.yml")
31
+ conf = if File.exist?(conf_path)
32
+ YAML.load_file(conf_path)
33
+ else
34
+ {}
35
+ end
36
+ @url = conf['url'] || conf[:url]
37
+ @default_service = conf['service'] || conf[:service]
38
+ @username = conf['username'] || conf[:username]
39
+ @password = conf['password'] || conf[:password]
40
+ @admin_proxy_username = conf['admin_proxy_username'] || conf[:admin_proxy_username]
41
+ @admin_proxy_password = conf['admin_proxy_password'] || conf[:admin_proxy_password]
42
+ end
43
+
44
+ # The given block will be executed using another account, as set in the confluence.yml file under admin_proxy_username
45
+ # and admin_proxy_password. This is useful when you want to execute some function that requires admin privileges. You
46
+ # will of course have to set up a corresponding account on your Confluence server with administrative rights.
47
+ def self.through_admin_proxy
48
+ super_connector = Confluence::Connector.new
49
+
50
+ raise "Cannot execute through_admin_proxy because the admin_proxy_username has not been set." unless super_connector.admin_proxy_username
51
+ raise "Cannot execute through_admin_proxy because the admin_proxy_password has not been set." unless super_connector.admin_proxy_password
52
+
53
+ super_connector.username = super_connector.admin_proxy_username
54
+ super_connector.password = super_connector.admin_proxy_password
55
+
56
+ normal_connector = Confluence::RemoteDataObject.connector
57
+ Confluence::RemoteDataObject.connector = super_connector
58
+
59
+ yield
60
+
61
+ Confluence::RemoteDataObject.connector = normal_connector
62
+ end
63
+ end
@@ -0,0 +1,187 @@
1
+ require 'active_support'
2
+ require 'confluence/confluence_connector'
3
+
4
+ # Abstract object representing some piece of data in Confluence.
5
+ # This must be overridden by a child class that defines values
6
+ # for the class attributes save_method and get_method and/or
7
+ # implements its own get and save methods.
8
+ class Confluence::RemoteDataObject
9
+ # include Reloadable
10
+
11
+ class_inheritable_accessor :attr_conversions, :readonly_attrs
12
+ class_inheritable_accessor :save_method, :get_method, :destroy_method
13
+
14
+ attr_accessor :attributes
15
+
16
+ attr_accessor :confluence, :encore
17
+
18
+ @@connector = nil
19
+
20
+ def self.connector=(connector)
21
+ @@connector = connector
22
+ end
23
+
24
+ def self.connector
25
+ @@connector
26
+ end
27
+
28
+ def self.confluence
29
+ raise "Cannot establish confluence connection because the connector has not been set." unless @@connector
30
+ @@connector.connect
31
+ end
32
+
33
+ def confluence
34
+ raise "Cannot establish confluence connection because the connector has not been set." unless @@connector
35
+ @@connector.connect
36
+ end
37
+
38
+ # TODO: encore-specific code like this probably shouldn't be here...
39
+ def self.encore
40
+ raise "Cannot establish confluence connection because the connector has not been set." unless @@connector
41
+ @@connector.connect("encore")
42
+ end
43
+
44
+ def encore
45
+ raise "Cannot establish confluence connection because the connector has not been set." unless @@connector
46
+ @@connector.connect("encore")
47
+ end
48
+
49
+ def initialize(data_object = nil)
50
+ self.attributes = {}
51
+ load_from_object(data_object) unless data_object.nil?
52
+ end
53
+
54
+ def load_from_object(data_object)
55
+ data_object.each do |attr, value|
56
+ if self.class.attr_conversions.has_key? attr.to_sym
57
+ value = self.send("as_#{attr_conversions[attr.to_sym]}", value)
58
+ end
59
+ self.send("#{attr}=", value)
60
+ end
61
+ end
62
+
63
+ def save
64
+ before_save if respond_to? :before_save
65
+
66
+ data = {} unless data
67
+
68
+ self.attributes.each do |attr,value|
69
+ data[attr.to_s] = value.to_s unless self.readonly_attrs.include? attr
70
+ end
71
+
72
+ raise NotImplementedError.new("Can't call #{self}.save because no @@save_method is defined for this class") unless self.save_method
73
+
74
+ self.confluence.send(self.class.send(:save_method), data)
75
+
76
+ # we need to reload because the version number has probably changed, we want the new ID, etc.
77
+ reload
78
+
79
+ after_save if respond_to? :after_save
80
+ end
81
+
82
+ def reload
83
+ before_reload if respond_to? :before_reload
84
+
85
+ if self.id
86
+ self.load_from_object(self.class.send(:get, self.id))
87
+ else
88
+ # We don't have an ID, so try to use alternate method for reloading. (This is for newly created records that may not yet have an id assigned)
89
+ raise NotImplementedError, "Can't reload this #{self.class} because it does not have an id and does not implement the reload_newly_created! method." unless self.respond_to? :reload_newly_created!
90
+ self.reload_newly_created!
91
+ end
92
+
93
+ after_reload if respond_to? :after_reload
94
+ end
95
+
96
+ def destroy
97
+ before_destroy if respond_to? :before_destroy
98
+
99
+ raise NotImplementedError.new("Can't call #{self}.destroy because no @@destroy_method is defined for this class") unless self.destroy_method
100
+ eval "confluence.#{self.destroy_method}(self.id.to_s)"
101
+
102
+ after_destroy if respond_to? :after_destroy
103
+ end
104
+
105
+ def [](attr)
106
+ self.attributes[attr]
107
+ end
108
+
109
+ def []=(attr, value)
110
+ self.attributes[attr] = value
111
+ end
112
+
113
+ def id
114
+ self[:id]
115
+ end
116
+
117
+ def method_missing(name, *args)
118
+ if name.to_s =~ /^(.*?)=$/
119
+ self[$1.intern] = args[0]
120
+ elsif name.to_s =~ /^[\w_]+$/
121
+ self[name]
122
+ else
123
+ raise NoMethodError, name.to_s
124
+ end
125
+ end
126
+
127
+ def ==(obj)
128
+ if obj.kind_of? self.class
129
+ self.attributes == obj.attributes
130
+ else
131
+ super
132
+ end
133
+ end
134
+
135
+ ### class methods #########################################################
136
+
137
+ def self.find(id)
138
+ r = get(id)
139
+ self.new(r)
140
+ end
141
+
142
+ ### type conversions ######################################################
143
+
144
+ # TODO: put these in a module?
145
+ def as_int(val)
146
+ val.to_i
147
+ end
148
+
149
+ def as_string(val)
150
+ val.to_s
151
+ end
152
+
153
+ def as_boolean(val)
154
+ val == "true"
155
+ end
156
+
157
+ def as_datetime(val)
158
+ if val.is_a?(String)
159
+ # for older versions of Confluence (pre 2.7?)
160
+ val =~ /\w{3} (\w{3}) (\d{2}) (\d{2}):(\d{2}):(\d{2}) (\w{3}) (\d{4})/
161
+ month = $1
162
+ day = $2
163
+ hour = $3
164
+ minute = $4
165
+ second = $5
166
+ tz = $6
167
+ year = $7
168
+ Time.local(year, month, day, hour, minute, second)
169
+ else
170
+ Time.local(val.year, val.month, val.day, val.hour, val.min, val.sec)
171
+ end
172
+ end
173
+
174
+ ###########################################################################
175
+
176
+ protected
177
+ # Returns the raw XML-RPC anonymous object with the data corresponding to
178
+ # the given id. This depends on the get_method class attribute, which must
179
+ # be defined for this method to work.
180
+ def self.get(id)
181
+ raise NotImplementedError.new("Can't call #{self}.get(#{id}) because no get_method is defined for this class") unless self.get_method
182
+ raise ArgumentError.new("You must specify a #{self} id!") unless id
183
+ confluence.log.debug("get_method for #{self} is #{self.get_method}")
184
+ obj = confluence.send(self.send(:get_method), id.to_s)
185
+ return obj
186
+ end
187
+ end
@@ -0,0 +1,83 @@
1
+ require 'xmlrpc/client'
2
+ require 'logger'
3
+
4
+ # A useful helper for running Confluence XML-RPC from Ruby. Takes care of
5
+ # adding the token to each method call (so you can call server.getSpaces()
6
+ # instead of server.getSpaces(token)). Also takes care of re-logging in
7
+ # if your login times out.
8
+ #
9
+ # Usage:
10
+ #
11
+ # server = Confluence::RPC.new("http://confluence.atlassian.com")
12
+ # server.login("user", "password")
13
+ # puts server.getSpaces()
14
+ #
15
+ module Confluence
16
+
17
+ class RPC
18
+ attr_accessor :log
19
+
20
+ def initialize(server_url, proxy = "confluence1")
21
+ server_url += "/rpc/xmlrpc" unless server_url[-11..-1] == "/rpc/xmlrpc"
22
+ @server_url = server_url
23
+ server = XMLRPC::Client.new2(server_url)
24
+ @conf = server.proxy(proxy)
25
+ @token = "12345"
26
+
27
+ @log = Logger.new($stdout)
28
+ end
29
+
30
+ def login(username, password)
31
+ log.info "Logging in as '#{username}'."
32
+ @user = username
33
+ @pass = password
34
+ do_login()
35
+ end
36
+
37
+ def method_missing(method_name, *args)
38
+ log.info "Calling #{method_name}(#{args.inspect})."
39
+ begin
40
+ @conf.send(method_name, *([@token] + args))
41
+ rescue XMLRPC::FaultException => e
42
+ log.error "#{e}: #{e.message}"
43
+ if (e.faultString.include?("InvalidSessionException"))
44
+ do_login
45
+ retry
46
+ else
47
+ raise RemoteException, e.respond_to?(:message) ? e.message : e
48
+ end
49
+ rescue
50
+ log.error "#{$!}"
51
+ raise $!
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def do_login()
58
+ begin
59
+ @token = @conf.login(@user, @pass)
60
+ rescue XMLRPC::FaultException => e
61
+ log.error "#{e}: #{e.faultString}"
62
+ raise RemoteAuthenticationException, e
63
+ end
64
+ end
65
+ end
66
+
67
+
68
+ class RemoteException < Exception
69
+ def initialize(msg = nil, type = nil)
70
+ if msg.kind_of? XMLRPC::FaultException
71
+ msg.faultString =~ /^.*?:\s(.*?):\s(.*)/
72
+ msg = $2
73
+ type = $1
74
+ end
75
+
76
+ super(msg)
77
+ @type = type
78
+ end
79
+ end
80
+
81
+ class RemoteAuthenticationException < RemoteException
82
+ end
83
+ end
@@ -0,0 +1,117 @@
1
+ # Allows for reading and writing a {metadata-list} macro in a page's content.
2
+ #
3
+ # For example, if your wiki content is:
4
+ #
5
+ # Hello, World!
6
+ # {metadata-list}
7
+ # || Foo | Bar |
8
+ # || Fruit | Orange |
9
+ # {metadata-list}
10
+ #
11
+ # You may do this (assuming that you have the above text stored in a variable
12
+ # called 'wiki_content'):
13
+ #
14
+ # m = Confluence::Metadata.new(wiki_content)
15
+ # puts m['Foo'] # outputs "Bar"
16
+ # puts m['Fruit'] # outputs "Orange"
17
+ #
18
+ # m['Fruit'] = "Banana"
19
+ # puts m['Fruit'] # outputs "Banana"
20
+ # m['Hello'] = "Goodbye"
21
+ #
22
+ # ... and your wiki_content now holds:
23
+ #
24
+ # Hello, World!
25
+ # {metadata-list}
26
+ # || Foo | Bar |
27
+ # || Fruit | Banana |
28
+ # || Hello | Goodbye |
29
+ # {metadata-list}
30
+ #
31
+ class Confluence::Metadata
32
+ include Enumerable
33
+
34
+ def initialize(page)
35
+ raise ArgumentError.new("Argument passed to Confluence::Metadata must be a Confluence::Page") unless page.kind_of? Confluence::Page
36
+ @page = page
37
+ end
38
+
39
+ def [](metadata_key)
40
+ extract_metadata_from_content[metadata_key]
41
+ end
42
+
43
+ def []=(metadata_key, value)
44
+ metadata = extract_metadata_from_content
45
+ metadata[metadata_key] = value
46
+ replace_metadata_in_content(metadata)
47
+ value
48
+ end
49
+
50
+ def each
51
+ metadata = extract_metadata_from_content
52
+ metadata.each{|k,v| yield k,v}
53
+ end
54
+
55
+ def include?(key)
56
+ not self.find{|k,v| k}.nil?
57
+ end
58
+ alias_method :has_key?, :include?
59
+
60
+ def empty?
61
+ extract_metadata_from_content.empty?
62
+ end
63
+
64
+ # Merges a hash (or some other Hash-like, Enumerable object) into
65
+ # this metadata. That is, key-value pairs from the given object will be added
66
+ # as metadata to this Confluence::Metadata's page, overriding any existing duplicate keys.
67
+ def merge!(data)
68
+ data.each do |k,v|
69
+ self[k] = v
70
+ end
71
+ end
72
+
73
+ private
74
+ def extract_metadata_from_content
75
+ return {} if @page.content.nil?
76
+
77
+ in_body = false
78
+ metadata = {}
79
+ @page.content.each do |line|
80
+ #TODO: handle other kinds of metadata macros
81
+ in_body = !in_body if line =~ /\{metadata-list\}/
82
+
83
+ if in_body
84
+ metadata[$1] = $2 if line =~ /\|\|\s*(.*?)\s*\|\s*(.*?)\s*\|/
85
+ end
86
+ end
87
+
88
+ return metadata
89
+ end
90
+
91
+ def replace_metadata_in_content(metadata)
92
+ #TODO: handle cases where there are multiple {metadata-list} macros in a page
93
+ #TODO: handle other kinds of metadata macros
94
+
95
+ macro_name = "{metadata-list}"
96
+
97
+ @page.content = "" if @page.content.nil?
98
+
99
+ @page.content += "\n#{macro_name}\n\n#{macro_name}" unless @page.content.include? macro_name
100
+
101
+ metadata_start = @page.content.index(macro_name) + macro_name.size
102
+ metadata_end = @page.content.index(macro_name, metadata_start) - 1
103
+
104
+ longest_key = 0
105
+ metadata.each do |key, val|
106
+ longest_key = key.size if key.size > longest_key
107
+ end
108
+
109
+ metadata_body = ""
110
+ metadata.each do |key, val|
111
+ padding = " " * (longest_key - key.size)
112
+ metadata_body += "|| #{key}#{padding} | #{val} |\n"
113
+ end
114
+
115
+ @page.content[metadata_start..metadata_end] = "\n"+metadata_body
116
+ end
117
+ end
@@ -0,0 +1,139 @@
1
+ require 'confluence/confluence_remote_data_object'
2
+
3
+ # Exposes a vaguely ActiveRecord-like interface for dealing with Confluence::Pages
4
+ # in Confluence.
5
+ class Confluence::Page < Confluence::RemoteDataObject
6
+
7
+ self.save_method = :storePage
8
+ self.get_method = :getPage
9
+ self.destroy_method = :removePage
10
+
11
+ self.attr_conversions = {
12
+ :id => :int,
13
+ :version => :int,
14
+ :parentId => :int,
15
+ :current => :boolean,
16
+ :homePage => :boolean,
17
+ :created => :datetime,
18
+ :modified => :datetime
19
+ }
20
+
21
+ self.readonly_attrs = [:current, :created, :modified, :contentStatus]
22
+
23
+ DEFAULT_SPACE = "encore"
24
+
25
+ def initialize(data_object = nil)
26
+ super
27
+ #self.creator = self.confluence_user unless self.include? :creator
28
+ end
29
+
30
+ def name=(new_name)
31
+ self.title = new_name
32
+ end
33
+
34
+ def name
35
+ self.title
36
+ end
37
+
38
+ def content=(new_content)
39
+ # make sure metadata doesn't get overwritten
40
+ old_metadata = self.metadata.entries if self.content
41
+ super
42
+ self.metadata.merge! old_metadata if old_metadata
43
+ end
44
+
45
+ def load_from_object(data_object)
46
+ super
47
+ self.modifier = self.confluence_username unless self.attributes.include? :modifier
48
+ end
49
+
50
+ def metadata
51
+ Confluence::Metadata.new(self)
52
+ end
53
+
54
+ def parent
55
+ self.class.find(self.parentId)
56
+ end
57
+
58
+ def to_s
59
+ self.title
60
+ end
61
+
62
+ def edit_group=(group)
63
+ encore.setPageEditGroup(self.title, group)
64
+ end
65
+
66
+ def edit_group
67
+ perm = get_permissions
68
+ return nil if perm.nil? or perm.empty?
69
+ perm.each do |p|
70
+ return p['lockedBy'] if p['lockType'] == 'Edit'
71
+ end
72
+ return nil
73
+ end
74
+
75
+ def view_group=(group)
76
+ encore.setPageViewGroup(self.title, group)
77
+ end
78
+
79
+ def view_group
80
+ perm = get_permissions
81
+ return nil if perm.nil? or perm.empty?
82
+ perm.each do |p|
83
+ return p['lockedBy'] if p['lockType'] == 'View'
84
+ end
85
+ return nil
86
+ end
87
+
88
+ def get_permissions
89
+ confluence.getPagePermissions(self.id.to_s)
90
+ end
91
+
92
+ ### callbacks ###############################################################
93
+
94
+ def before_save
95
+ raise "Cannot save this page because it has no title and/or space." unless self.title and self.space
96
+ end
97
+
98
+ #############################################################################
99
+
100
+ ### class methods ###########################################################
101
+
102
+ def self.find_by_name(name, space = DEFAULT_SPACE)
103
+ find_by_title(name, space)
104
+ end
105
+
106
+ def self.find_by_title(title, space = DEFAULT_SPACE)
107
+ begin
108
+ r = confluence.getPage(space, title)
109
+ rescue Confluence::RemoteException => e
110
+ # FIXME: dangerous to be checking based on error string like this...
111
+ if e.message =~ /does not exist/
112
+ return nil
113
+ else
114
+ raise e
115
+ end
116
+ end
117
+ self.new(r)
118
+ end
119
+
120
+
121
+ #############################################################################
122
+
123
+ protected
124
+ def self.metadata_accessor(accessor, metadata_key)
125
+ f = <<-END
126
+ def #{accessor.to_s}
127
+ self.metadata['#{metadata_key}']
128
+ end
129
+ def #{accessor.to_s}=(val)
130
+ self.metadata['#{metadata_key}'] = val.gsub("\n", " ")
131
+ end
132
+ END
133
+ module_eval f
134
+ end
135
+
136
+ def reload_newly_created!
137
+ self.load_from_object(confluence.getPage(self.space, self.title))
138
+ end
139
+ end
@@ -0,0 +1,108 @@
1
+ require 'confluence/confluence_remote_data_object'
2
+
3
+ class Confluence::User < Confluence::RemoteDataObject
4
+
5
+ self.save_method = :editUser
6
+ # TODO: implement :create_method ('addUser')
7
+ self.get_method = :getUser
8
+ self.destroy_method = :removeUser
9
+
10
+ self.readonly_attrs = ['username', 'name']
11
+ self.attr_conversions = {}
12
+
13
+ @groups
14
+
15
+ def id
16
+ self.username
17
+ end
18
+
19
+ def id=(new_id)
20
+ self.username = new_id
21
+ end
22
+
23
+ def username
24
+ self.name
25
+ end
26
+
27
+ def username=(new_username)
28
+ self.username=(new_username)
29
+ end
30
+
31
+ def groups
32
+ # groups are cached for the lifetime of the user object or until a group-modifying method is called
33
+ # FIXME: This is probably a bad idea, since the user's groups may be changed outside of the user object
34
+ # ... currently it's not a serious problem, since this is unlikely to happen within the object's
35
+ # short lifetime, but it may be problematic if start storing the user object in a cache or in the
36
+ # session.
37
+ @groups ||= confluence.getUserGroups(username)
38
+ end
39
+
40
+ def in_group?(group)
41
+ groups.include? group
42
+ end
43
+
44
+ def add_to_group(group)
45
+ @groups = nil # reset cached group list
46
+ confluence.addUserToGroup(username, group)
47
+ end
48
+
49
+ def remove_from_group(group)
50
+ @groups = nil # reset cached group list
51
+ confluence.removeUserFromGroup(username, group)
52
+ end
53
+
54
+ def has_permission?(permtype, page)
55
+ if permtype == :edit
56
+ group_or_name = page.edit_group
57
+ else
58
+ group_or_name = page.view_group
59
+ end
60
+
61
+ return true if group_or_name.nil?
62
+ return true if group_or_name == username
63
+ return in_group?(group_or_name)
64
+ end
65
+
66
+ def to_s
67
+ self.username
68
+ end
69
+
70
+ def to_wiki
71
+ "[~#{self.username}]"
72
+ end
73
+
74
+ ### class methods #########################################################
75
+
76
+ def self.find_by_username(username)
77
+ find(username)
78
+ end
79
+
80
+ # DEPRECATED: this method is confusing since it could be taken as meaning "find by first/last name"
81
+ def self.find_by_name(username)
82
+ find_by_username(username)
83
+ end
84
+
85
+ def self.find_by_email(email)
86
+ usernames = confluence.getActiveUsers(true)
87
+ usernames.each do |username|
88
+ user = find_by_username(username)
89
+ return user if user.email == email
90
+ end
91
+
92
+ return nil
93
+ end
94
+
95
+ def self.find_all
96
+ # FIXME: this is really slow... we should probably just look in the confluence database instead
97
+ usernames = find_all_usernames
98
+ usernames.collect{|u| find_by_username(u)}
99
+ end
100
+
101
+ def self.find_all_usernames
102
+ confluence.getActiveUsers(true)
103
+ end
104
+
105
+ class NoSuchUser < Exception
106
+ end
107
+
108
+ end
data/lib/nippocf.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "nippocf/version"
2
+
3
+ module Nippocf
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,139 @@
1
+ require 'nippocf'
2
+ require 'keychain'
3
+ require 'io/console'
4
+ require 'confluence/confluence_connector'
5
+ require 'redcarpet'
6
+ require 'tmpdir'
7
+ require 'nokogiri'
8
+ require 'optparse'
9
+
10
+ if ENV['SOCKS_PROXY']
11
+ require 'socksify'
12
+ server, port = ENV['SOCKS_PROXY'].split(':')
13
+ TCPSocket::socks_server = server
14
+ TCPSocket::socks_port = port
15
+ end
16
+
17
+ module Nippocf
18
+ class CLI
19
+ KEYCHAIN_SERVICE_NAME = 'Nippocf'
20
+
21
+ def initialize(argv)
22
+ @argv = argv
23
+ parse_options!
24
+ end
25
+
26
+ def run
27
+ rpc = connect_to_confl
28
+
29
+ blog_entries = rpc.getBlogEntries("~#{username}")
30
+ title = time.strftime('%Y/%m/%d')
31
+ entry_summary = blog_entries.find {|entry| entry['title'] == title }
32
+
33
+ markdown_text = ""
34
+
35
+ if entry_summary
36
+ entry = rpc.getBlogEntry(entry_summary['id'])
37
+ # entry contains markdown content
38
+ doc = Nokogiri::HTML(entry['content'])
39
+ elm = doc.css('div.markdown_source').first
40
+ markdown_text = elm.text if elm
41
+ else
42
+ entry = {
43
+ "content" => "",
44
+ "title" => title,
45
+ "space" => "~#{username}",
46
+ "author" => username,
47
+ "publishDate" => time,
48
+ "permissions" => "0"
49
+ }
50
+ end
51
+
52
+ Dir.mktmpdir do |dir|
53
+ filename = File.join(dir, 'editing.md')
54
+ open(filename, 'w') do |f|
55
+ f.write markdown_text
56
+ end
57
+ editor = ENV['EDITOR'] || 'vim'
58
+ system "#{editor} #{filename}"
59
+ markdown_text = File.read(filename)
60
+ end
61
+
62
+ markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, :autolink => true, :space_after_headers => true)
63
+ entry['content'] = markdown.render(markdown_text)
64
+ entry['content'] << "<div class=\"markdown_source\" style=\"display:none\">#{markdown_text}</div>"
65
+ rpc.storeBlogEntry(entry)
66
+ end
67
+
68
+ private
69
+ def password_for_user(username)
70
+ keychain = Keychain.generic_passwords.where(
71
+ service: KEYCHAIN_SERVICE_NAME,
72
+ account: username).first
73
+ keychain && keychain.password
74
+ end
75
+
76
+ def set_password_for_user(username)
77
+ print "Password for #{username}: "
78
+ password = STDIN.noecho(&:gets).chomp
79
+ puts
80
+ Keychain.generic_passwords.create(
81
+ service: KEYCHAIN_SERVICE_NAME,
82
+ password: password,
83
+ account: username)
84
+ password
85
+ end
86
+
87
+ def connect_to_confl
88
+ password = password_for_user(username)
89
+ unless password
90
+ password = set_password_for_user(username)
91
+ end
92
+ connector = Confluence::Connector.new(
93
+ url: ENV['CONFL_URL'],
94
+ username: username,
95
+ password: password
96
+ )
97
+ connector.connect('confluence2').tap do |rpc|
98
+ unless @debug
99
+ rpc.log = Logger.new(StringIO.new)
100
+ end
101
+ end
102
+ end
103
+
104
+ def username
105
+ ENV['CONFL_USERNAME']
106
+ end
107
+
108
+ def time
109
+ now = Time.now
110
+ year = now.year
111
+ month = now.month
112
+ day = now.day
113
+
114
+ if @argv.size >= 1
115
+ date_str = @argv.first
116
+ elms = date_str.split('/')
117
+ case elms.size
118
+ when 1
119
+ day, = elms
120
+ when 2
121
+ month, day = elms
122
+ when 3
123
+ year, month, day = elms
124
+ else
125
+ raise "Invalid arg"
126
+ end
127
+ end
128
+
129
+ Time.local(year, month, day)
130
+ end
131
+
132
+ def parse_options!
133
+ opt = OptionParser.new
134
+ opt.on('--debug') { @debug = true }
135
+ opt.parse!(@argv)
136
+ end
137
+ end
138
+ end
139
+
@@ -0,0 +1,3 @@
1
+ module Nippocf
2
+ VERSION = "0.0.1"
3
+ end
data/nippocf.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'nippocf/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "nippocf"
8
+ spec.version = Nippocf::VERSION
9
+ spec.authors = ["Ryota Arai"]
10
+ spec.email = ["ryota.arai@gree.net"]
11
+ spec.summary = "Write nippo (daily report in Japanese) on Atlassian Confluence in Markdown"
12
+ spec.homepage = "https://github.com/ryotarai/nippocf"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "redcarpet"
21
+ spec.add_dependency "nokogiri"
22
+ spec.add_dependency "ruby-keychain"
23
+ spec.add_dependency "socksify"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.5"
26
+ spec.add_development_dependency "rake"
27
+ end
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nippocf
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ryota Arai
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redcarpet
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ruby-keychain
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: socksify
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.5'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.5'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description:
98
+ email:
99
+ - ryota.arai@gree.net
100
+ executables:
101
+ - nippocf
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - Gemfile
107
+ - LICENSE.txt
108
+ - README.md
109
+ - Rakefile
110
+ - bin/nippocf
111
+ - lib/confluence/confluence_connector.rb
112
+ - lib/confluence/confluence_remote_data_object.rb
113
+ - lib/confluence/confluence_rpc.rb
114
+ - lib/confluence/metadata.rb
115
+ - lib/confluence/page.rb
116
+ - lib/confluence/user.rb
117
+ - lib/nippocf.rb
118
+ - lib/nippocf/cli.rb
119
+ - lib/nippocf/version.rb
120
+ - nippocf.gemspec
121
+ homepage: https://github.com/ryotarai/nippocf
122
+ licenses:
123
+ - MIT
124
+ metadata: {}
125
+ post_install_message:
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubyforge_project:
141
+ rubygems_version: 2.2.0
142
+ signing_key:
143
+ specification_version: 4
144
+ summary: Write nippo (daily report in Japanese) on Atlassian Confluence in Markdown
145
+ test_files: []