nippocf 0.0.1

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