qarioz-confluencer 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +135 -0
- data/lib/confluence/attachment.rb +36 -0
- data/lib/confluence/blog_entry.rb +30 -0
- data/lib/confluence/bookmark.rb +85 -0
- data/lib/confluence/client.rb +145 -0
- data/lib/confluence/error.rb +4 -0
- data/lib/confluence/findable.rb +23 -0
- data/lib/confluence/page.rb +160 -0
- data/lib/confluence/record.rb +107 -0
- data/lib/confluence/session.rb +88 -0
- data/lib/confluence/space.rb +59 -0
- data/lib/confluence/user.rb +19 -0
- data/lib/confluencer.rb +23 -0
- data/script/console +11 -0
- data/script/console_init.rb +5 -0
- metadata +93 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Secret Sauce Partners, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
= confluencer
|
2
|
+
|
3
|
+
Easy to use, ActiveRecord-like classes to find spaces, pages and bookmarks as well as adding new ones and updating existing ones.
|
4
|
+
It uses ruby's method missing to translate methods to Confluence XMLRPC API (v2) call.
|
5
|
+
|
6
|
+
For Confluence XML RPC API documentation, please take a look at these links:
|
7
|
+
|
8
|
+
{Confluence XML-RPC and SOAP APIs}[https://developer.atlassian.com/display/CONFDEV/Confluence+XML-RPC+and+SOAP+APIs]
|
9
|
+
|
10
|
+
{Remote Confluence Data Objects}[https://developer.atlassian.com/display/CONFDEV/Remote+Confluence+Data+Objects]
|
11
|
+
|
12
|
+
{https://developer.atlassian.com/display/CONFDEV/Remote+Confluence+Methods}[https://developer.atlassian.com/display/CONFDEV/Remote+Confluence+Methods]
|
13
|
+
|
14
|
+
== Installation
|
15
|
+
=== Install from rubygems
|
16
|
+
gem install qarioz-confluencer
|
17
|
+
=== Build and install from source
|
18
|
+
Clone the repository, build the gem, install the gem.
|
19
|
+
|
20
|
+
git clone git://github.com/qarioz/confluencer.git
|
21
|
+
cd confluencer
|
22
|
+
gem install jeweler log4r
|
23
|
+
gem build confluencer.gemspec
|
24
|
+
gem install qarioz-confluencer-0.6.0.gem
|
25
|
+
=== Include in Gemfile
|
26
|
+
Or if you are using Gemfile, add this to your Gemfile
|
27
|
+
|
28
|
+
gem 'qarioz-confluencer', :git => 'git://github.com/qarioz/confluencer.git'
|
29
|
+
|
30
|
+
== Usage
|
31
|
+
|
32
|
+
This is the usage example for confluencer
|
33
|
+
|
34
|
+
require 'confluencer'
|
35
|
+
begin
|
36
|
+
#login
|
37
|
+
client = Confluence::Client.new(url: "https://mywiki.atlassian.net/wiki/")
|
38
|
+
client.login("username", "password")
|
39
|
+
# no need to save resulting token; api handles it
|
40
|
+
|
41
|
+
# get all spaces
|
42
|
+
all_spaces = client.get_spaces
|
43
|
+
# => [{'url', 'name', 'key', 'type'}]
|
44
|
+
|
45
|
+
# get specific space
|
46
|
+
test_space = client.get_space( "TestSpace")
|
47
|
+
# => {'url', 'name', 'key', 'type', 'homepage'}
|
48
|
+
|
49
|
+
# create a new space
|
50
|
+
s = Confluence::Space.new
|
51
|
+
# => {}
|
52
|
+
|
53
|
+
# get pages from a space
|
54
|
+
ps = client.get_pages( "TestSpace")
|
55
|
+
# => [{ 'url', 'id', 'version', 'title', 'space', 'parentId', 'permissions'}]
|
56
|
+
page_id = ps[0]['id'] # => "8345678"
|
57
|
+
pidcl = page_id.class # => String
|
58
|
+
|
59
|
+
# get comments from a page
|
60
|
+
comments = client.get_comments( page_id)
|
61
|
+
# => [{ 'modifier', 'creator', 'content', 'title',
|
62
|
+
# 'parentId', 'id', 'pageId', 'modified', 'url', 'created'}]
|
63
|
+
|
64
|
+
# create a comment
|
65
|
+
new_comment = client.add_comment( {pageId: page_id, title: "ruby sez", content: "blah blah blah"})
|
66
|
+
|
67
|
+
# create a label for a page
|
68
|
+
label = client.add_label_by_name( "new-label", page_id)
|
69
|
+
# => true
|
70
|
+
|
71
|
+
#update a page
|
72
|
+
npage = client.store_page(
|
73
|
+
space: "TestSpace", title: "yapage29", content: "Here is the content, also from ruby")
|
74
|
+
# => {'id', 'current', 'content', 'title', 'version', 'modifier',
|
75
|
+
# 'url', 'homePage', 'creator', 'contentStatus', 'modified',
|
76
|
+
# 'created', 'space', 'parentId', 'permissions'}
|
77
|
+
# if "yapage29" exists, throws {Confluence::Error}Could not save or update record
|
78
|
+
rescue Exception => e
|
79
|
+
puts "error: " + e.to_s
|
80
|
+
end
|
81
|
+
|
82
|
+
=== Troubleshooting
|
83
|
+
==== SSL Problem
|
84
|
+
|
85
|
+
If you get SSL Problem while using this gem on windows, follow the steps below to fix it.
|
86
|
+
|
87
|
+
Uncaught exception: SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed
|
88
|
+
|
89
|
+
This assumes your have already installed the [Rails Installer](http://railsinstaller.org) for Windows.
|
90
|
+
|
91
|
+
Download the ruby script to your *Desktop* folder from [https://gist.github.com/raw/867550/win_fetch_cacerts.rb](https://gist.github.com/raw/867550/win_fetch_cacerts.rb). Then in your command prompt, execute the ruby script:
|
92
|
+
|
93
|
+
ruby "%USERPROFILE%\Desktop\win_fetch_cacerts.rb"
|
94
|
+
|
95
|
+
Or copy paste the script below:
|
96
|
+
|
97
|
+
require 'net/http'
|
98
|
+
|
99
|
+
# create a path to the file "C:\RailsInstaller\cacert.pem"
|
100
|
+
cacert_file = File.join(%w{c: RailsInstaller cacert.pem})
|
101
|
+
|
102
|
+
Net::HTTP.start("curl.haxx.se") do |http|
|
103
|
+
resp = http.get("/ca/cacert.pem")
|
104
|
+
if resp.code == "200"
|
105
|
+
open(cacert_file, "wb") { |file| file.write(resp.body) }
|
106
|
+
puts "\n\nA bundle of certificate authorities has been installed to"
|
107
|
+
puts "C:\\RailsInstaller\\cacert.pem\n"
|
108
|
+
puts "* Please set SSL_CERT_FILE in your current command prompt session with:"
|
109
|
+
puts " set SSL_CERT_FILE=C:\\RailsInstaller\\cacert.pem"
|
110
|
+
puts "* To make this a permanent setting, add it to Environment Variables"
|
111
|
+
puts " under Control Panel -> Advanced -> Environment Variables"
|
112
|
+
else
|
113
|
+
abort "\n\n>>>> A cacert.pem bundle could not be downloaded."
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
Now make ruby aware of your certificate authority bundle by setting `SSL_CERT_FILE`. To set this in your current command prompt session, type:
|
118
|
+
|
119
|
+
set SSL_CERT_FILE=C:\RailsInstaller\cacert.pem
|
120
|
+
|
121
|
+
To make this a permanent setting, add this in your [control panel](http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/environment_variables.mspx?mfr=true).
|
122
|
+
|
123
|
+
==== The Manual Way (Boring)
|
124
|
+
|
125
|
+
Download the `cacert.pem` file from [http://curl.haxx.se/ca/cacert.pem](http://curl.haxx.se/ca/cacert.pem). Save this file to `C:\RailsInstaller\cacert.pem`.
|
126
|
+
|
127
|
+
Now make ruby aware of your certificate authority bundle by setting `SSL_CERT_FILE`. To set this in your current command prompt session, type:
|
128
|
+
|
129
|
+
set SSL_CERT_FILE=C:\RailsInstaller\cacert.pem
|
130
|
+
|
131
|
+
To make this a permanent setting, add this in your [control panel](http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/environment_variables.mspx?mfr=true).
|
132
|
+
|
133
|
+
== Copyright
|
134
|
+
|
135
|
+
Copyright (c) 2010 Secret Sauce Partners, Inc. See LICENSE for details.
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Confluence
|
2
|
+
class Attachment < Record
|
3
|
+
extend Findable
|
4
|
+
|
5
|
+
record_attr_accessor :id => :attachment_id
|
6
|
+
record_attr_accessor :pageId => :page_id
|
7
|
+
record_attr_accessor :fileName => :filename
|
8
|
+
record_attr_accessor :fileSize => :filesize
|
9
|
+
record_attr_accessor :contentType => :content_type
|
10
|
+
record_attr_accessor :title, :creator, :created, :url, :comment
|
11
|
+
|
12
|
+
attr_writer :data
|
13
|
+
|
14
|
+
def data
|
15
|
+
@data ||= client.getAttachmentData(page_id, filename, "0")
|
16
|
+
end
|
17
|
+
|
18
|
+
def store
|
19
|
+
# reinitialize attachment after storing it
|
20
|
+
initialize(client.addAttachment(page_id, self.to_hash, XMLRPC::Base64.new(@data)))
|
21
|
+
|
22
|
+
# return self
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def remove
|
27
|
+
client.removeAttachment(page_id, filename)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.find_criteria(args)
|
31
|
+
if args.key? :page_id and args.key? :filename
|
32
|
+
self.new(client.getAttachment(args[:page_id], args[:filename], args[:version] || "0"))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Confluence
|
2
|
+
class BlogEntry < Record
|
3
|
+
extend Findable
|
4
|
+
|
5
|
+
record_attr_accessor :id => :entry_id
|
6
|
+
record_attr_accessor :space
|
7
|
+
record_attr_accessor :title, :content
|
8
|
+
record_attr_accessor :publishDate
|
9
|
+
record_attr_accessor :url
|
10
|
+
|
11
|
+
def store
|
12
|
+
# reinitialize blog entry after storing it
|
13
|
+
initialize(client.storeBlogEntry(self.to_hash))
|
14
|
+
end
|
15
|
+
|
16
|
+
def remove
|
17
|
+
client.removePage(self.entry_id)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def self.find_criteria(args)
|
23
|
+
if args.key? :id
|
24
|
+
self.new(client.getBlogEntry(args[:id]))
|
25
|
+
elsif args.key? :space
|
26
|
+
client.getBlogEntries(args[:space]).collect { |summary| BlogEntry.find(:id => summary["id"]) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Confluence
|
2
|
+
class Bookmark < Page
|
3
|
+
attr_accessor :bookmark_url, :description
|
4
|
+
|
5
|
+
BOOKMARK_REGEXP = /\{bookmark.*\}[^\{]*\{bookmark\}/
|
6
|
+
BOOKMARK_URL_REGEXP = /\{bookmark:url=([^\}]+)\}/
|
7
|
+
DESCRIPTION_REGEXP = /\{bookmark.*\}([^\{]*)\{bookmark\}/
|
8
|
+
|
9
|
+
def initialize(hash)
|
10
|
+
# set and delete bookmark_url and description coming from hash
|
11
|
+
@bookmark_url = hash.delete :bookmark_url
|
12
|
+
@description = hash.delete :description
|
13
|
+
|
14
|
+
# initialize page
|
15
|
+
super(hash)
|
16
|
+
|
17
|
+
if content
|
18
|
+
# if no bookmark_url from hash, initialize from content
|
19
|
+
unless @bookmark_url
|
20
|
+
@bookmark_url = content[BOOKMARK_URL_REGEXP, 1]
|
21
|
+
@description = content[DESCRIPTION_REGEXP, 1]
|
22
|
+
end
|
23
|
+
|
24
|
+
# remove {bookmark} macro from content
|
25
|
+
content.gsub!(BOOKMARK_REGEXP, "")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](attr)
|
30
|
+
case attr
|
31
|
+
when :bookmark_url
|
32
|
+
@bookmark_url
|
33
|
+
when :description
|
34
|
+
@description
|
35
|
+
else
|
36
|
+
super(attr)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def []=(attr, value)
|
41
|
+
case attr
|
42
|
+
when :bookmark_url
|
43
|
+
@bookmark_url = value
|
44
|
+
when :description
|
45
|
+
@description = value
|
46
|
+
else
|
47
|
+
super(attr, value)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def store(args = {})
|
52
|
+
# always set .bookmarks as the parent page
|
53
|
+
self.parent_id = Page.find(:space => space, :title => Space::BOOKMARKS_PAGE_TITLE).page_id
|
54
|
+
|
55
|
+
# continue with storing the page
|
56
|
+
super(args)
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_hash
|
60
|
+
page_hash = super
|
61
|
+
|
62
|
+
page_hash["content"] << "\n" unless page_hash["content"].empty?
|
63
|
+
page_hash["content"] << bookmark_content
|
64
|
+
|
65
|
+
page_hash
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def bookmark_content
|
71
|
+
"{bookmark:url=#{@bookmark_url}}#{@description}{bookmark}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.find_criteria(args)
|
75
|
+
result = super(args) || begin
|
76
|
+
if args.key? :space
|
77
|
+
space = Space.find :key => args[:space]
|
78
|
+
space.bookmarks
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
result
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'xmlrpc/client'
|
2
|
+
|
3
|
+
# Module containing Confluence-related classes.
|
4
|
+
module Confluence
|
5
|
+
# Originally confluence4r, available at: http://confluence.atlassian.com/display/DISC/Confluence4r
|
6
|
+
|
7
|
+
# A useful helper for running Confluence XML-RPC from Ruby. Takes care of
|
8
|
+
# adding the token to each method call (so you can call server.getSpaces()
|
9
|
+
# instead of server.getSpaces(token)).
|
10
|
+
#
|
11
|
+
# Usage:
|
12
|
+
#
|
13
|
+
# client = Confluence::Client.new(:url => "http://confluence.atlassian.com")
|
14
|
+
# client.login("user", "password")
|
15
|
+
# p client.getSpaces
|
16
|
+
#
|
17
|
+
class Client
|
18
|
+
PREFIX = "confluence2"
|
19
|
+
XMLRPC_SUFFIX = "/rpc/xmlrpc"
|
20
|
+
|
21
|
+
attr_reader :url, :username, :token
|
22
|
+
|
23
|
+
# Initializes a new client with the given arguments.
|
24
|
+
#
|
25
|
+
# ==== Parameters
|
26
|
+
# arguments<Hash>:: Described below.
|
27
|
+
#
|
28
|
+
# ==== Arguments
|
29
|
+
# :url - The url of the Confluence instance. The trailing '/rpc/xmlrpc' path is optional.
|
30
|
+
# :token - An existing session token to reuse.
|
31
|
+
#
|
32
|
+
def initialize(arguments = {})
|
33
|
+
@url = arguments[:url]
|
34
|
+
@token = arguments[:token]
|
35
|
+
|
36
|
+
Log4r::MDC.put('token', @token || 'nil')
|
37
|
+
log.info "initialized client (:url => #{@url}, :token => #{@token || 'nil'})"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns true, if the client has a session token.
|
41
|
+
#
|
42
|
+
def has_token?
|
43
|
+
!@token.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
# Logs in and returns the newly acquired session token.
|
47
|
+
#
|
48
|
+
# ==== Parameters
|
49
|
+
# username<String>:: The username.
|
50
|
+
# password<String>:: The password.
|
51
|
+
#
|
52
|
+
def login(username, password)
|
53
|
+
handle_fault do
|
54
|
+
if @token = proxy.login(username, password)
|
55
|
+
Log4r::MDC.put('token', @token)
|
56
|
+
log.info "logged in as '#{username}' and acquired token."
|
57
|
+
|
58
|
+
@username = username
|
59
|
+
@password = password
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
@token
|
64
|
+
end
|
65
|
+
|
66
|
+
# Logs out and invalidates the session token.
|
67
|
+
#
|
68
|
+
def logout
|
69
|
+
handle_fault do
|
70
|
+
@token = nil if @token and result = proxy.logout(@token)
|
71
|
+
log.info "logged out"
|
72
|
+
Log4r::MDC.put('token', 'nil')
|
73
|
+
result
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Translates every call into XMLRPC calls.
|
78
|
+
#
|
79
|
+
def method_missing(method_name, *args)
|
80
|
+
handle_fault do
|
81
|
+
if args.empty?
|
82
|
+
log.debug "#{method_name}"
|
83
|
+
else
|
84
|
+
log.debug "#{method_name}(#{(args.collect {|a| a.inspect}).join(', ')})"
|
85
|
+
end
|
86
|
+
|
87
|
+
begin
|
88
|
+
method_name = camel_case_lower(method_name.to_s).to_sym if !(method_name.match(/[A-Z]/))
|
89
|
+
result = proxy.send(method_name, *([@token] + args))
|
90
|
+
log.debug(result.inspect)
|
91
|
+
result
|
92
|
+
rescue EOFError => e
|
93
|
+
log.warn "Could not complete XMLRPC call, retrying... Error: #{e.inspect}"
|
94
|
+
retry
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# Returns the Confluence::Client logger.
|
102
|
+
def log
|
103
|
+
Log4r::Logger[Confluence::Client.to_s] || Log4r::Logger.root
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns the Confluence XMLRPC endpoint url.
|
107
|
+
#
|
108
|
+
def xmlrpc_url
|
109
|
+
unless @url[-11..-1] == XMLRPC_SUFFIX
|
110
|
+
@url + XMLRPC_SUFFIX
|
111
|
+
else
|
112
|
+
@url
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns the XMLRPC client proxy for the Confluence API v1.
|
117
|
+
#
|
118
|
+
def proxy
|
119
|
+
@proxy ||= XMLRPC::Client.new_from_uri(xmlrpc_url).proxy(PREFIX)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Yields and translates any XMLRPC::FaultExceptions raised by Confluence to Confluence::Errors.
|
123
|
+
#
|
124
|
+
def handle_fault(&block)
|
125
|
+
begin
|
126
|
+
block.call
|
127
|
+
rescue XMLRPC::FaultException => e
|
128
|
+
log.warn message = e.faultString.rpartition(':').last.strip
|
129
|
+
|
130
|
+
case message
|
131
|
+
when /Transaction rolled back/
|
132
|
+
raise Confluence::Error, "Could not save or update record."
|
133
|
+
else
|
134
|
+
raise Confluence::Error, message
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Make camel case with lower first letter
|
140
|
+
def camel_case_lower(original_string)
|
141
|
+
original_string.split('_').inject([]){ |buffer,e| buffer.push(buffer.empty? ? e : e.capitalize) }.join
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Confluence
|
2
|
+
module Findable
|
3
|
+
# Finds records by the given criteria.
|
4
|
+
#
|
5
|
+
# ==== Parameters
|
6
|
+
# args<Hash>:: The search arguments.
|
7
|
+
def find(args)
|
8
|
+
if args.has_key?(:all) && !respond_to?(:find_all)
|
9
|
+
raise "Cannot find all #{self.class.name.downcase}s, find by criteria instead."
|
10
|
+
end
|
11
|
+
|
12
|
+
begin
|
13
|
+
case args
|
14
|
+
when :all
|
15
|
+
find_all
|
16
|
+
when Hash
|
17
|
+
find_criteria(args)
|
18
|
+
end
|
19
|
+
rescue Confluence::Error
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module Confluence
|
2
|
+
class Page < Record
|
3
|
+
INVALID_TITLE_CHARS = ":@/\\|^#;[]{}<>"
|
4
|
+
|
5
|
+
class Details < Hash
|
6
|
+
REGEXP = /\{details:label=([^\}]+)\}([^\{}]*)\{details\}/m
|
7
|
+
PAIR_REGEXP = /([^:]+):([^\n]+)/m
|
8
|
+
|
9
|
+
attr_reader :label
|
10
|
+
|
11
|
+
def initialize(args)
|
12
|
+
@label = args[:label]
|
13
|
+
|
14
|
+
parse(args[:content])
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
# details macro
|
19
|
+
content = "{details:label=#{label}}\n"
|
20
|
+
|
21
|
+
each_pair do |key, value|
|
22
|
+
content << "#{key}:#{value}\n"
|
23
|
+
end
|
24
|
+
|
25
|
+
# end of details macro
|
26
|
+
content << "{details}\n"
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def parse(content)
|
32
|
+
if content && content =~ REGEXP
|
33
|
+
# match label and the key/value pairs
|
34
|
+
@label, pairs = content.match(REGEXP).captures
|
35
|
+
|
36
|
+
pairs.strip.lines.each do |line|
|
37
|
+
if line =~ PAIR_REGEXP
|
38
|
+
self[$1.to_sym] = $2.strip
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class DetailsCollection < Hash
|
46
|
+
def initialize(content)
|
47
|
+
if content
|
48
|
+
content.gsub!(Details::REGEXP) do |content|
|
49
|
+
self[$1.to_sym] = Details.new(:content => content)
|
50
|
+
""
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def [](key)
|
56
|
+
super(key) or self[key] = Details.new(:label => key)
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_s
|
60
|
+
values.join("\n")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
extend Findable
|
65
|
+
|
66
|
+
record_attr_accessor :id => :page_id
|
67
|
+
record_attr_accessor :parentId => :parent_id
|
68
|
+
record_attr_accessor :space
|
69
|
+
record_attr_accessor :title, :creator, :modifier, :content
|
70
|
+
record_attr_accessor :created, :modified, :version
|
71
|
+
record_attr_accessor :url
|
72
|
+
|
73
|
+
attr_accessor :details
|
74
|
+
|
75
|
+
def initialize(hash)
|
76
|
+
super(hash)
|
77
|
+
|
78
|
+
@details = DetailsCollection.new(content)
|
79
|
+
end
|
80
|
+
|
81
|
+
def children(klass = self.class)
|
82
|
+
children = client.getChildren(page_id)
|
83
|
+
children.collect { |hash| klass.find(:id => hash["id"]) } if children
|
84
|
+
end
|
85
|
+
|
86
|
+
def attachments
|
87
|
+
attachments = client.getAttachments(page_id)
|
88
|
+
attachments.collect { |hash| Attachment.new(hash) } if attachments
|
89
|
+
end
|
90
|
+
|
91
|
+
def store(args = {})
|
92
|
+
unless self.version
|
93
|
+
# check for existing page by id or title
|
94
|
+
existing_page = if page_id
|
95
|
+
Page.find :id => page_id
|
96
|
+
else
|
97
|
+
Page.find :space => space, :title => title
|
98
|
+
end
|
99
|
+
|
100
|
+
# take page_id and version from existing page if available
|
101
|
+
if existing_page
|
102
|
+
if args[:recreate_if_exists]
|
103
|
+
# remove existing page
|
104
|
+
existing_page.remove
|
105
|
+
else
|
106
|
+
# update page with page_id and version info
|
107
|
+
self.page_id = existing_page.page_id
|
108
|
+
self.version = existing_page.version
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# reinitialize page after storing it
|
114
|
+
initialize(client.storePage(self.to_hash))
|
115
|
+
|
116
|
+
# return self
|
117
|
+
self
|
118
|
+
end
|
119
|
+
|
120
|
+
def remove
|
121
|
+
client.removePage(page_id)
|
122
|
+
end
|
123
|
+
|
124
|
+
def add_attachment(filename, content_type, data = IO.read(filename), comment = "")
|
125
|
+
attachment = Attachment.new :pageId => page_id, :fileName => filename, :contentType => content_type, :comment => comment
|
126
|
+
attachment.data = data
|
127
|
+
attachment.store
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_hash
|
131
|
+
# record hash
|
132
|
+
record_hash = super
|
133
|
+
|
134
|
+
# always include content in hash
|
135
|
+
record_hash["content"] ||= ""
|
136
|
+
|
137
|
+
# prepend details sections before content
|
138
|
+
record_hash["content"].insert(0, details.to_s)
|
139
|
+
|
140
|
+
# result
|
141
|
+
record_hash
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def self.find_criteria(args)
|
147
|
+
if args.key? :id
|
148
|
+
self.new(client.getPage(args[:id]))
|
149
|
+
elsif args.key?(:space) && args.key?(:title)
|
150
|
+
self.new(client.getPage(args[:space], args[:title]))
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class String
|
157
|
+
def to_page_title
|
158
|
+
self.delete(Confluence::Page::INVALID_TITLE_CHARS).strip
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Confluence
|
2
|
+
# Base class for working with Confluence records.
|
3
|
+
#
|
4
|
+
class Record
|
5
|
+
class << self
|
6
|
+
# The client used for Confluence API calls.
|
7
|
+
#
|
8
|
+
def client
|
9
|
+
raise "Confluence client is unavailable. Did you forget to use Confluence::Session.new?" unless @@client
|
10
|
+
@@client
|
11
|
+
end
|
12
|
+
|
13
|
+
# Sets the client.
|
14
|
+
#
|
15
|
+
def client=(value)
|
16
|
+
@@client = value
|
17
|
+
end
|
18
|
+
|
19
|
+
# Defines an attr_accessor for a Record attribute.
|
20
|
+
#
|
21
|
+
def record_attr_accessor(*args)
|
22
|
+
attributes = {}
|
23
|
+
|
24
|
+
# iterate through each argument
|
25
|
+
args.each do |arg|
|
26
|
+
attributes = case arg
|
27
|
+
when Symbol
|
28
|
+
{ arg => arg }
|
29
|
+
when Hash
|
30
|
+
arg
|
31
|
+
else
|
32
|
+
break
|
33
|
+
end
|
34
|
+
|
35
|
+
attributes.each_pair do |key, name|
|
36
|
+
class_eval %Q{
|
37
|
+
def #{name}
|
38
|
+
self[:#{key}]
|
39
|
+
end
|
40
|
+
|
41
|
+
def #{name}=(value)
|
42
|
+
self[:#{key}] = value
|
43
|
+
end
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the the Confluence API client.
|
51
|
+
#
|
52
|
+
def client
|
53
|
+
Record.client
|
54
|
+
end
|
55
|
+
|
56
|
+
# Initializes a new record.
|
57
|
+
#
|
58
|
+
# ==== Parameters
|
59
|
+
# hash<Hash>:: A hash containing the attributes and its values. Keys can be Strings or Symbols.
|
60
|
+
def initialize(hash = {})
|
61
|
+
@attributes = {}
|
62
|
+
|
63
|
+
# iterate through each key/value pair and set attribute keyed by symbol
|
64
|
+
hash.each_pair do |key, value|
|
65
|
+
self[key.to_sym] = value
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def [](attr)
|
70
|
+
@attributes[attr]
|
71
|
+
end
|
72
|
+
|
73
|
+
def []=(attr, value)
|
74
|
+
@attributes[attr] = value
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns the id of the record.
|
78
|
+
#
|
79
|
+
def record_id
|
80
|
+
self[:id]
|
81
|
+
end
|
82
|
+
|
83
|
+
# Retrieves the labels of the record.
|
84
|
+
#
|
85
|
+
def labels
|
86
|
+
@labels ||= client.getLabelsById(record_id).collect {|label| label["name"]}
|
87
|
+
end
|
88
|
+
|
89
|
+
# Sets the labels of the record.
|
90
|
+
#
|
91
|
+
def labels=(value)
|
92
|
+
removed_labels = labels - value
|
93
|
+
added_labels = value - labels
|
94
|
+
|
95
|
+
client.removeLabelByName(removed_labels.join(" "), record_id) unless removed_labels.empty?
|
96
|
+
client.addLabelByName(added_labels.join(" "), record_id) unless added_labels.empty?
|
97
|
+
|
98
|
+
@labels = value
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_hash
|
102
|
+
hash = Hash.new
|
103
|
+
@attributes.each { |key, value| hash[key.to_s] = value }
|
104
|
+
hash
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Confluence
|
2
|
+
# Wraps a Confluence::Client and manages the lifetime of a session.
|
3
|
+
#
|
4
|
+
class Session
|
5
|
+
attr_reader :client
|
6
|
+
|
7
|
+
# Initializes a new session with the given arguments and sets it for other classes like Confluence::Page.
|
8
|
+
#
|
9
|
+
# If a block is given to initialize, initialize yields with the Confluence::Client and automatically logs out of the session afterwards.
|
10
|
+
# Otherwise Session#destroy should be called after finished.
|
11
|
+
#
|
12
|
+
# ==== Parameters
|
13
|
+
# arguments<Hash>:: Described below.
|
14
|
+
#
|
15
|
+
# ==== Arguments
|
16
|
+
# :url - The url of the Confluence instance.
|
17
|
+
# :username - The username.
|
18
|
+
# :password - The password.
|
19
|
+
#
|
20
|
+
def initialize(arguments = {})
|
21
|
+
raise ArgumentError, "Required argument 'url' is missing." unless arguments.key? :url
|
22
|
+
|
23
|
+
@client = Confluence::Client.new(arguments)
|
24
|
+
|
25
|
+
unless @client.has_token?
|
26
|
+
raise ArgumentError, "Required argument 'username' is missing." unless arguments.key? :username
|
27
|
+
raise ArgumentError, "Required argument 'password' is missing." unless arguments.key? :password
|
28
|
+
|
29
|
+
@client.login(arguments[:username], arguments[:password])
|
30
|
+
end
|
31
|
+
|
32
|
+
# set client for records
|
33
|
+
Confluence::Record.client = @client
|
34
|
+
|
35
|
+
# yield if block was given and destroy afterwards
|
36
|
+
if block_given?
|
37
|
+
yield @client
|
38
|
+
|
39
|
+
self.destroy
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the current session token.
|
44
|
+
#
|
45
|
+
def token
|
46
|
+
client.token if client
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns the currently logged in username.
|
50
|
+
#
|
51
|
+
def username
|
52
|
+
client.username if client
|
53
|
+
end
|
54
|
+
|
55
|
+
# Logs in and acquire a new token after destroying the previous session.
|
56
|
+
#
|
57
|
+
# ==== Parameters
|
58
|
+
# arguments<Hash>:: Described below.
|
59
|
+
#
|
60
|
+
# ==== Arguments
|
61
|
+
# :username - The username.
|
62
|
+
# :password - The password.
|
63
|
+
#
|
64
|
+
def login(arguments)
|
65
|
+
raise ArgumentError, "Required argument 'username' is missing." unless arguments.key? :username
|
66
|
+
raise ArgumentError, "Required argument 'password' is missing." unless arguments.key? :password
|
67
|
+
|
68
|
+
# log out first
|
69
|
+
client.logout if client
|
70
|
+
|
71
|
+
# log in with credentials
|
72
|
+
@client.login(arguments[:username], arguments[:password])
|
73
|
+
end
|
74
|
+
|
75
|
+
# Destroys the session by logging out and resets other classes like Confluence::Page.
|
76
|
+
#
|
77
|
+
def destroy
|
78
|
+
# invalidate the token
|
79
|
+
client.logout
|
80
|
+
|
81
|
+
# client is not valid anymore
|
82
|
+
@client = nil
|
83
|
+
|
84
|
+
# reset client for records
|
85
|
+
Confluence::Record.client = nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Confluence
|
2
|
+
# A Confluence space.
|
3
|
+
class Space < Record
|
4
|
+
extend Findable
|
5
|
+
|
6
|
+
BOOKMARKS_PAGE_TITLE = ".bookmarks"
|
7
|
+
|
8
|
+
record_attr_accessor :key, :name, :url, :description
|
9
|
+
record_attr_accessor :homePage => :homepage
|
10
|
+
|
11
|
+
def bookmark_page
|
12
|
+
@bookmark_page ||= Page.find :space => self.key, :title => BOOKMARKS_PAGE_TITLE
|
13
|
+
end
|
14
|
+
|
15
|
+
def bookmarks
|
16
|
+
@bookmarks ||= bookmark_page ? bookmark_page.children(Bookmark) : []
|
17
|
+
end
|
18
|
+
|
19
|
+
def blog_entries
|
20
|
+
client.getBlogEntries(self.key).collect {|summary| BlogEntry.new(client.getBlogEntry(summary["id"]))}
|
21
|
+
end
|
22
|
+
|
23
|
+
def find_page(args)
|
24
|
+
if args.key? :title
|
25
|
+
Page.find :space => self.key, :title => args[:title]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_page(args)
|
30
|
+
args[:parent_title] ||= "Home"
|
31
|
+
|
32
|
+
# check if page already exists
|
33
|
+
find_page(args) or begin
|
34
|
+
# page does not exist yet, create it
|
35
|
+
page = Confluence::Page.new :space => self.key, :title => args[:title]
|
36
|
+
|
37
|
+
# look for the parent by title, set parentId if found
|
38
|
+
if parent_page = find_page(:title => args[:parent_title])
|
39
|
+
page.parent_id = parent_page.page_id
|
40
|
+
end
|
41
|
+
|
42
|
+
# store the page
|
43
|
+
page.store
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def self.find_all
|
50
|
+
client.getSpaces.collect { |summary| Space.find(:key => summary["key"]) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.find_criteria(args)
|
54
|
+
if args.key? :key
|
55
|
+
Space.new(client.getSpace(args[:key]))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Confluence
|
2
|
+
class User < Record
|
3
|
+
extend Findable
|
4
|
+
|
5
|
+
record_attr_accessor :name, :fullname, :email, :url
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def self.find_all
|
10
|
+
client.getActiveUsers(true).collect { |name| User.find :name => name }
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.find_criteria(args)
|
14
|
+
if args.key? :name
|
15
|
+
User.new(client.getUser(args[:name]))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/confluencer.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# ensure that lib is in the load path
|
2
|
+
$:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'log4r'
|
6
|
+
|
7
|
+
require 'confluence/error'
|
8
|
+
|
9
|
+
require 'confluence/client'
|
10
|
+
require 'confluence/session'
|
11
|
+
|
12
|
+
require 'confluence/findable'
|
13
|
+
require 'confluence/record'
|
14
|
+
require 'confluence/user'
|
15
|
+
require 'confluence/space'
|
16
|
+
require 'confluence/page'
|
17
|
+
require 'confluence/bookmark'
|
18
|
+
require 'confluence/blog_entry'
|
19
|
+
require 'confluence/attachment'
|
20
|
+
|
21
|
+
module Confluencer
|
22
|
+
VERSION = "0.6.0"
|
23
|
+
end
|
data/script/console
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
libs = " -r irb/completion"
|
4
|
+
|
5
|
+
libs << " -r #{File.dirname(__FILE__) + '/../lib/confluencer.rb'}"
|
6
|
+
|
7
|
+
# load console_init
|
8
|
+
libs << " -r #{File.dirname(__FILE__) + '/console_init.rb'}"
|
9
|
+
|
10
|
+
puts "Loading confluencer..."
|
11
|
+
exec "irb #{libs} --simple-prompt"
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: qarioz-confluencer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.6.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Secret Sauce Partners, Inc.
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2010-06-02 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.3.0
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.3.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: log4r
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.1.7
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.1.7
|
46
|
+
description: ActiveRecord-like classes to access Confluence through XMLRPC.
|
47
|
+
email: github@secretsaucepartners.com
|
48
|
+
executables: []
|
49
|
+
extensions: []
|
50
|
+
extra_rdoc_files: []
|
51
|
+
files:
|
52
|
+
- script/console
|
53
|
+
- script/console_init.rb
|
54
|
+
- lib/confluencer.rb
|
55
|
+
- lib/confluence/client.rb
|
56
|
+
- lib/confluence/bookmark.rb
|
57
|
+
- lib/confluence/error.rb
|
58
|
+
- lib/confluence/page.rb
|
59
|
+
- lib/confluence/space.rb
|
60
|
+
- lib/confluence/blog_entry.rb
|
61
|
+
- lib/confluence/attachment.rb
|
62
|
+
- lib/confluence/record.rb
|
63
|
+
- lib/confluence/user.rb
|
64
|
+
- lib/confluence/findable.rb
|
65
|
+
- lib/confluence/session.rb
|
66
|
+
- LICENSE
|
67
|
+
- README.rdoc
|
68
|
+
homepage: http://github.com/sspinc/confluencer
|
69
|
+
licenses: []
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options:
|
72
|
+
- --charset=UTF-8
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ! '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: 1.3.6
|
87
|
+
requirements: []
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.8.25
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Useful classes to manage Confluence.
|
93
|
+
test_files: []
|