rt-client 0.5.0 → 0.6.4

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.
Files changed (3) hide show
  1. data/rt/client.rb +350 -363
  2. data/rt/rtxmlsrv.rb +0 -1
  3. metadata +4 -115
data/rt/client.rb CHANGED
@@ -2,178 +2,160 @@
2
2
 
3
3
  require "rubygems"
4
4
  require "rest_client"
5
- require "tmail"
6
- require "iconv"
7
- require 'mime/types' # requires both nokogiri and rcov. Yuck.
8
- require 'date'
9
- require 'pp'
10
5
 
11
- ## A ruby library API to Request Tracker's REST interface. Requires the
12
- ## rubygems rest-client, tmail and mime-types to be installed. You can
13
- ## create a file name .rtclientrc in the same directory as client.rb with a
14
- ## default server/user/pass to connect to RT as, so that you don't have to
15
- ## specify it/update it in lots of different scripts.
6
+ ##A ruby library API to Request Tracker's REST interface. Requires the
7
+ ##rubygems rest-client, mail and mime-types to be installed. You can
8
+ ##create a file name .rtclientrc in the same directory as client.rb with a
9
+ ##default server/user/pass to connect to RT as, so that you don't have to
10
+ ##specify it/update it in lots of different scripts.
16
11
  ##
17
12
  ## Thanks to Brian McArdle for patch dealing with spaces in Custom Fields.
18
13
  ## To reference custom fields in RT that have spaces with rt-client, use an
19
- ## underscore in the rt-client code for the hash key.
20
- #
21
- ## TODO: Streaming, chunking attachments in compose method
14
+ ## underscore in the rt-client code, e.g. "CF.{Has_Space}"
15
+ ##
16
+ ## Thanks to Robert Vinson for 1.9.x compatibility when I couldn't be buggered
17
+ ## and a great job of refactoring the old mess into something with much fewer
18
+ ## dependencies.
19
+ ##
20
+ ##TODO: Streaming, chunking attachments in compose method
22
21
  #
23
22
  # See each method for sample usage. To use this, "gem install rt-client" and
24
23
  #
25
24
  # require "rt/client"
26
-
27
25
  class RT_Client
28
26
 
29
- UA = "Mozilla/5.0 ruby RT Client Interface 0.5.0"
30
- attr_reader :status, :site, :version, :cookies, :server, :user, :cookie
31
-
32
- # Create a new RT_Client object. Load up our stored cookie and check it.
33
- # Log into RT again if needed and store the new cookie. You can specify
34
- # login and cookie storage directories in 3 different ways:
35
- # 1. Explicity during object creation
36
- # 2. From a .rtclientrc file in the working directory of your ruby program
37
- # 3. From a .rtclientrc file in the same directory as the library itself
38
- #
39
- # These are listed in order of priority; if you have explicit parameters,
40
- # they are always used, even if you have .rtclientrc files. If there
41
- # is both an .rtclientrc in your program's working directory and
42
- # in the library directory, the one from your program's working directory
43
- # is used. If no parameters are specified either explicity or by use
44
- # of a .rtclientrc, then the defaults of "rt_user", "rt_pass" are used
45
- # with a default server of "http://localhost", and cookies are stored
46
- # in the directory where the library resides.
47
- #
48
- # rt= RT_Client.new( :server => "https://tickets.ambulance.com/",
49
- # :user => "rt_user",
50
- # :pass => "rt_pass",
51
- # :cookies => "/my/cookie/dir" )
52
- #
53
- # rt= RT_Client.new # use defaults from .rtclientrc
54
- #
55
- # .rtclientrc format:
56
- # server=<RT server>
57
- # user=<RT user>
58
- # pass=<RT password>
59
- # cookies=<directory>
60
- def initialize(*params)
61
- @boundary = "----xYzZY#{rand(1000000).to_s}xYzZY"
62
- @version = "0.4.0"
63
- @status = "Not connected"
64
- @server = "http://localhost/"
65
- @user = "rt_user"
66
- @pass = "rt_pass"
67
- @cookies = Dir.pwd
68
- config_file = Dir.pwd + "/.rtclientrc"
69
- config = ""
70
- if File.file?(config_file)
71
- config = File.read(config_file)
72
- else
73
- config_file = File.dirname(__FILE__) + "/.rtclientrc"
74
- config = File.read(config_file) if File.file?(config_file)
75
- end
76
- @server = $~[1] if config =~ /\s*server\s*=\s*(.*)$/i
77
- @user = $~[1] if config =~ /^\s*user\s*=\s*(.*)$/i
78
- @pass = $~[1] if config =~ /^\s*pass\s*=\s*(.*)$/i
79
- @cookies = $~[1] if config =~ /\s*cookies\s*=\s*(.*)$/i
80
- @resource = "#{@server}REST/1.0/"
81
- if params.class == Array && params[0].class == Hash
82
- param = params[0]
83
- @user = param[:user] if param.has_key? :user
84
- @pass = param[:pass] if param.has_key? :pass
85
- if param.has_key? :server
86
- @server = param[:server]
87
- @server += "/" if @server !~ /\/$/
88
- @resource = "#{@server}REST/1.0/"
89
- end
90
- @cookies = param[:cookies] if param.has_key? :cookies
91
- end
92
- @login = { :user => @user, :pass => @pass }
93
- cookiejar = "#{@cookies}/RT_Client.#{@user}.cookie" # cookie location
94
- cookiejar.untaint
95
- if File.file? cookiejar
96
- @cookie = File.read(cookiejar).chomp
97
- headers = { 'User-Agent' => UA,
98
- 'Content-Type' => "application/x-www-form-urlencoded",
99
- 'Cookie' => @cookie }
100
- else
101
- headers = { 'User-Agent' => UA,
102
- 'Content-Type' => "application/x-www-form-urlencoded" }
103
- @cookie = ""
104
- end
105
-
27
+ UA = "Mozilla/5.0 ruby RT Client Interface 0.6.4"
28
+ attr_reader :status, :site, :version, :cookies, :server, :user, :cookie
106
29
 
107
- site = RestClient::Resource.new(@resource, :headers => headers, :timeout => 120)
108
- data = site.post "" # a null post just to check that we are logged in
109
-
110
- if @cookie.length == 0 or data =~ /401/ # we're not logged in
111
- data = site.post @login, :headers => headers
112
- # puts data
113
- @cookie = data.headers[:set_cookie].to_s.split('; ')[0]
114
- # write the new cookie
115
- if @cookie !~ /nil/
116
- f = File.new(cookiejar,"w")
117
- f.puts @cookie
118
- f.close
119
- end
120
- end
121
- headers = { 'User-Agent' => UA,
122
- 'Content-Type' => "multipart/form-data; boundary=#{@boundary}",
123
- 'Cookie' => @cookie }
124
- @site = RestClient::Resource.new(@resource, :headers => headers)
125
- @status = data
126
- self.untaint
127
- end
30
+ # Create a new RT_Client object. Load up our stored cookie and check it.
31
+ # Log into RT again if needed and store the new cookie. You can specify
32
+ # login and cookie storage directories in 3 different ways:
33
+ # 1. Explicity during object creation
34
+ # 2. From a .rtclientrc file in the working directory of your ruby program
35
+ # 3. From a .rtclientrc file in the same directory as the library itself
36
+ #
37
+ # These are listed in order of priority; if you have explicit parameters,
38
+ # they are always used, even if you have .rtclientrc files. If there
39
+ # is both an .rtclientrc in your program's working directory and
40
+ # in the library directory, the one from your program's working directory
41
+ # is used. If no parameters are specified either explicity or by use
42
+ # of a .rtclientrc, then the defaults of "rt_user", "rt_pass" are used
43
+ # with a default server of "http://localhost", and cookies are stored
44
+ # in the directory where the library resides.
45
+ #
46
+ # rt= RT_Client.new( :server => "https://tickets.ambulance.com/",
47
+ # :user => "rt_user",
48
+ # :pass => "rt_pass",
49
+ # :cookies => "/my/cookie/dir" )
50
+ #
51
+ # rt= RT_Client.new # use defaults from .rtclientrc
52
+ #
53
+ # .rtclientrc format:
54
+ # server=<RT server>
55
+ # user=<RT user>
56
+ # pass=<RT password>
57
+ # cookies=<directory>
58
+ def initialize(*params)
59
+ @boundary = "----xYzZY#{rand(1000000).to_s}xYzZY"
60
+ @version = "0.4.0"
61
+ @status = "Not connected"
62
+ @server = "http://localhost/"
63
+ @user = "rt_user"
64
+ @pass = "rt_pass"
65
+ @cookies = Dir.pwd
66
+ config_file = Dir.pwd + "/.rtclientrc"
67
+ config = ""
68
+ if File.file?(config_file)
69
+ config = File.read(config_file)
70
+ else
71
+ config_file = File.dirname(__FILE__) + "/.rtclientrc"
72
+ config = File.read(config_file) if File.file?(config_file)
73
+ end
74
+ @server = $~[1] if config =~ /\s*server\s*=\s*(.*)$/i
75
+ @user = $~[1] if config =~ /^\s*user\s*=\s*(.*)$/i
76
+ @pass = $~[1] if config =~ /^\s*pass\s*=\s*(.*)$/i
77
+ @cookies = $~[1] if config =~ /\s*cookies\s*=\s*(.*)$/i
78
+ @resource = "#{@server}REST/1.0/"
79
+ if params.class == Array && params[0].class == Hash
80
+ param = params[0]
81
+ @user = param[:user] if param.has_key? :user
82
+ @pass = param[:pass] if param.has_key? :pass
83
+ if param.has_key? :server
84
+ @server = param[:server]
85
+ @server += "/" if @server !~ /\/$/
86
+ @resource = "#{@server}REST/1.0/"
87
+ end
88
+ @cookies = param[:cookies] if param.has_key? :cookies
89
+ end
90
+ @login = { :user => @user, :pass => @pass }
91
+ cookiejar = "#{@cookies}/RT_Client.#{@user}.cookie" # cookie location
92
+ cookiejar.untaint
93
+ if File.file? cookiejar
94
+ @cookie = File.read(cookiejar).chomp
95
+ headers = { 'User-Agent' => UA,
96
+ 'Content-Type' => "application/x-www-form-urlencoded",
97
+ 'Cookie' => @cookie }
98
+ else
99
+ headers = { 'User-Agent' => UA,
100
+ 'Content-Type' => "application/x-www-form-urlencoded" }
101
+ @cookie = ""
102
+ end
128
103
 
129
- # gets the detail for a single ticket/user. If its a ticket, its without
130
- # history or attachments (to get those use the history method) . If no
131
- # type is specified, ticket is assumed. takes a single parameter
132
- # containing the ticket/user id, and returns a hash of RT Fields => values
133
- #
134
- # hash = rt.show(822)
135
- # hash = rt.show("822")
136
- # hash = rt.show("ticket/822")
137
- # hash = rt.show(:id => 822)
138
- # hash = rt.show(:id => "822")
139
- # hash = rt.show(:id => "ticket/822")
140
- # hash = rt.show("user/#{login}")
141
- # email = rt.show("user/somebody")["emailaddress"]
142
- def show(id)
143
- id = id[:id] if id.class == Hash
144
- id = id.to_s
145
- type = "ticket"
146
- sid = id
147
- if id =~ /(\w+)\/(.+)/
148
- type = $~[1]
149
- sid = $~[2]
150
- end
151
- reply = {}
152
- if type.downcase == 'user'
153
- resp = @site["#{type}/#{sid}"].get
154
- else
155
- resp = @site["#{type}/#{sid}/show"].get
156
- end
157
- resp.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n\n/,"") # toss the HTTP response
158
- resp.gsub!(/\n\n/,"\n") # remove double spacing, TMail stops at a blank line
159
- while resp.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/) #replace CF spaces with underscores
160
- resp.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
161
- end
162
- return {:error => resp, } if resp =~ /does not exist./
163
- th = TMail::Mail.parse(resp)
164
- resp += "\nrtclientend:" # to make the pattern match on the last key in the response
165
- th.each_header do |k,v|
166
- reply["#{k}"] = v.to_s
167
- kk = k.gsub(/\{/,"\\\{")
168
- kk.gsub!(/\}/,"\\\}")
169
- pattern = "^" + kk + ": (.*?)^[\\w\\.\\{\\}]+:"
170
- temp = resp.match(/#{pattern}/mi) # TMail strips line breaks
171
- reply["#{k}"] = temp[1].rstrip if temp.class != NilClass
172
- end
173
- reply
174
- end
175
104
 
176
- # gets a list of ticket links for a ticket.
105
+ site = RestClient::Resource.new(@resource, :headers => headers, :timeout => 120)
106
+ data = site.post "" # a null post just to check that we are logged in
107
+
108
+ if @cookie.length == 0 or data =~ /401/ # we're not logged in
109
+ data = site.post @login, :headers => headers
110
+ # puts data
111
+ @cookie = data.headers[:set_cookie].first.split('; ')[0]
112
+ # write the new cookie
113
+ if @cookie !~ /nil/
114
+ f = File.new(cookiejar,"w")
115
+ f.puts @cookie
116
+ f.close
117
+ end
118
+ end
119
+ headers = { 'User-Agent' => UA,
120
+ 'Content-Type' => "multipart/form-data; boundary=#{@boundary}",
121
+ 'Cookie' => @cookie }
122
+ @site = RestClient::Resource.new(@resource, :headers => headers)
123
+ @status = data
124
+ self.untaint
125
+ end
126
+
127
+ # gets the detail for a single ticket/user. If its a ticket, its without
128
+ # history or attachments (to get those use the history method) . If no
129
+ # type is specified, ticket is assumed. takes a single parameter
130
+ # containing the ticket/user id, and returns a hash of RT Fields => values
131
+ #
132
+ # hash = rt.show(822)
133
+ # hash = rt.show("822")
134
+ # hash = rt.show("ticket/822")
135
+ # hash = rt.show(:id => 822)
136
+ # hash = rt.show(:id => "822")
137
+ # hash = rt.show(:id => "ticket/822")
138
+ # hash = rt.show("user/#{login}")
139
+ # email = rt.show("user/somebody")["emailaddress"]
140
+ def show(id)
141
+ id = id[:id] if id.class == Hash
142
+ id = id.to_s
143
+ type = "ticket"
144
+ sid = id
145
+ if id =~ /(\w+)\/(.+)/
146
+ type = $~[1]
147
+ sid = $~[2]
148
+ end
149
+ reply = {}
150
+ if type.downcase == 'user'
151
+ resp = @site["#{type}/#{sid}"].get
152
+ else
153
+ resp = @site["#{type}/#{sid}/show"].get
154
+ end
155
+ reply = response_to_h(resp)
156
+ end
157
+
158
+ # gets a list of ticket links for a ticket.
177
159
  # takes a single parameter containing the ticket id,
178
160
  # and returns a hash of RT Fields => values
179
161
  #
@@ -194,47 +176,37 @@ class RT_Client
194
176
  end
195
177
  reply = {}
196
178
  resp = @site["ticket/#{sid}/links/show"].get
197
- resp.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n\n/,"") # toss the HTTP response
198
- resp.gsub!(/\n\n/,"\n") # remove double spacing, TMail stops at a blank line
199
- while resp.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/) #replace CF spaces with underscores
200
- resp.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
201
- end
202
179
  return {:error => resp, } if resp =~ /does not exist./
203
- th = TMail::Mail.parse(resp)
204
- th.each_header do |k,v|
205
- reply["#{k}"] = v.to_s
206
- end
207
- reply
180
+ reply = response_to_h(resp)
208
181
  end
209
182
 
210
- # Creates a new ticket. Requires a hash that contains RT form fields as
211
- # the keys. Capitalization is important; use :Queue, not :queue. You
212
- # will need at least :Queue to create a ticket. For a full list of fields
213
- # you can use, try "/opt/rt3/bin/rt edit ticket/1". Returns the newly
214
- # created ticket number, or a complete REST response.
215
- #
216
- # id = rt.create( :Queue => "Customer Service",
217
- # :Cc => "somebody\@email.com",
218
- # :Subject => "I've fallen and I can't get up",
219
- # :Text => "I think my hip is broken.\nPlease help.",
220
- # :"CF.{CustomField}" => "Urgent",
221
- # :Attachment => "/tmp/broken_hip.jpg" )
222
- def create(field_hash)
223
- field_hash[:id] = "ticket/new"
224
- payload = compose(field_hash)
225
- puts "Payload for new ticket:"
226
- puts payload
227
- resp = @site['ticket/new/edit'].post payload
228
- new_id = resp.match(/Ticket\s*(\d+)/)
229
- if new_id.class == MatchData
230
- new_ticket = new_id[1]
231
- else
232
- new_ticket = resp
233
- end
234
- new_ticket # return the ticket number, or the full REST response
235
- end
236
-
237
- # create a new user. Requires a hash of RT fields => values. Returns
183
+ # Creates a new ticket. Requires a hash that contains RT form fields as
184
+ # the keys. Capitalization is important; use :Queue, not :queue. You
185
+ # will need at least :Queue to create a ticket. For a full list of fields
186
+ # you can use, try "/opt/rt3/bin/rt edit ticket/1". Returns the newly
187
+ # created ticket number, or a complete REST response.
188
+ #
189
+ # id = rt.create( :Queue => "Customer Service",
190
+ # :Cc => "somebody\@email.com",
191
+ # :Subject => "I've fallen and I can't get up",
192
+ # :Text => "I think my hip is broken.\nPlease help.",
193
+ # :"CF.{CustomField}" => "Urgent",
194
+ def create(field_hash)
195
+ field_hash[:id] = "ticket/new"
196
+ payload = compose(field_hash)
197
+ puts "Payload for new ticket:"
198
+ puts payload
199
+ resp = @site['ticket/new/edit'].post payload
200
+ new_id = resp.match(/Ticket\s*(\d+)/)
201
+ if new_id.class == MatchData
202
+ new_ticket = new_id[1]
203
+ else
204
+ new_ticket = resp
205
+ end
206
+ new_ticket # return the ticket number, or the full REST response
207
+ end
208
+
209
+ # create a new user. Requires a hash of RT fields => values. Returns
238
210
  # the newly created user ID, or the full REST response if there is an error.
239
211
  # For a full list of possible parameters that you can specify, look at
240
212
  # "/opt/rt/bin/rt edit user/1"
@@ -331,14 +303,8 @@ class RT_Client
331
303
  raise "RT_Client.usersearch requires a user email in the 'EmailAddress' key."
332
304
  end
333
305
  resp = @site["user/#{email}"].get
334
- resp.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n\n/,"") # toss the HTTP response
335
- reply = {}
336
306
  return reply if resp =~ /No user named/
337
- th = TMail::Mail.parse(resp)
338
- th.each_header do |k,v|
339
- reply["#{k}"] = v.to_s
340
- end
341
- reply
307
+ reply = response_to_h(resp)
342
308
  end
343
309
 
344
310
  # correspond on a ticket. Requires a hash, which must have an :id key
@@ -363,98 +329,93 @@ class RT_Client
363
329
  payload = compose(field_hash)
364
330
  @site["ticket/#{id}/comment"].post payload
365
331
  end
366
-
367
- # Get a list of tickets matching some criteria.
368
- # Takes a string Ticket-SQL query and an optional "order by" parameter.
369
- # The order by is an RT field, prefix it with + for ascending
370
- # or - for descending.
371
- # Returns a nested array of arrays containing [ticket number, subject]
372
- # The outer array is in the order requested.
373
- #
374
- # hash = rt.list(:query => "Queue = 'Sales'")
375
- # hash = rt.list("Queue='Sales'")
376
- # hash = rt.list(:query => "Queue = 'Sales'", :order => "-Id")
377
- # hash = rt.list("Queue='Sales'","-Id")
378
- def list(*params)
379
- query = params[0]
380
- order = ""
381
- if params.size > 1
382
- order = params[1]
383
- end
384
- if params[0].class == Hash
385
- params = params[0]
386
- query = params[:query] if params.has_key? :query
387
- order = params[:order] if params.has_key? :order
388
- end
389
- reply = []
390
- resp = @site["search/ticket/?query=#{URI.escape(query)}&orderby=#{order}&format=s"].get
391
- raise "Invalid query (#{query})" if resp =~ /Invalid query/
392
- resp = resp.split("\n") # convert to array of lines
393
- resp.each do |line|
394
- f = line.match(/^(\d+):\s*(.*)/)
395
- reply.push [f[1],f[2]] if f.class == MatchData
396
- end
397
- reply
398
- end
399
332
 
400
- # A more extensive(expensive) query then the list method. Takes the same
401
- # parameters as the list method; a string Ticket-SQL query and optional
402
- # order, but returns a lot more information. Instead of just the ID and
403
- # subject, you get back an array of hashes, where each hash represents
404
- # one ticket, indentical to what you get from the show method (which only
405
- # acts on one ticket). Use with caution; this can take a long time to
406
- # execute.
407
- #
408
- # array = rt.query("Queue='Sales'")
409
- # array = rt.query(:query => "Queue='Sales'",:order => "+Id")
410
- # array = rt.query("Queue='Sales'","+Id")
411
- # => array[0] = { "id" => "123", "requestors" => "someone@..", etc etc }
412
- # => array[1] = { "id" => "126", "requestors" => "someone@else..", etc etc }
413
- # => array[0]["id"] = "123"
414
- def query(*params)
415
- query = params[0]
416
- order = ""
417
- if params.size > 1
418
- order = params[1]
419
- end
420
- if params[0].class == Hash
421
- params = params[0]
422
- query = params[:query] if params.has_key? :query
423
- order = params[:order] if params.has_key? :order
424
- end
425
- replies = []
426
- resp = @site["search/ticket/?query=#{URI.escape(query)}&orderby=#{order}&format=l"].get
427
- return replies if resp =~/No matching results./
428
- raise "Invalid query (#{query})" if resp =~ /Invalid query/
429
- resp.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n\n/,"") # strip HTTP response
430
- tickets = resp.split("\n--\n") # -- occurs between each ticket
431
- tickets.each do |ticket|
432
- ticket.gsub!(/^\n/,"") # strip leading blank lines
433
- ticket.gsub!(/\n\n/,"\n") # remove blank lines for TMail
434
- while ticket.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/) #replace CF spaces with underscores
435
- ticket.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
436
- end
437
- th = TMail::Mail.parse(ticket)
438
- reply = {}
439
- th.each_header do |k,v|
440
- case k
441
- when 'created','due','told','lastupdated','started'
442
- begin
443
- vv = DateTime.parse(v.to_s)
444
- reply["#{k}"] = vv.strftime("%Y-%m-%d %H:%M:%S")
445
- rescue ArgumentError
446
- reply["#{k}"] = v.to_s
447
- end
448
- else
449
- reply["#{k}"] = v.to_s
450
- end
451
- end
452
- replies.push reply
453
- end
454
- replies
455
- end
456
-
457
- # Get a list of history transactions for a ticket. Takes a ticket ID and
333
+ # Get a list of tickets matching some criteria.
334
+ # Takes a string Ticket-SQL query and an optional "order by" parameter.
335
+ # The order by is an RT field, prefix it with + for ascending
336
+ # or - for descending.
337
+ # Returns a nested array of arrays containing [ticket number, subject]
338
+ # The outer array is in the order requested.
339
+ #
340
+ # hash = rt.list(:query => "Queue = 'Sales'")
341
+ # hash = rt.list("Queue='Sales'")
342
+ # hash = rt.list(:query => "Queue = 'Sales'", :order => "-Id")
343
+ # hash = rt.list("Queue='Sales'","-Id")
344
+ def list(*params)
345
+ query = params[0]
346
+ order = ""
347
+ if params.size > 1
348
+ order = params[1]
349
+ end
350
+ if params[0].class == Hash
351
+ params = params[0]
352
+ query = params[:query] if params.has_key? :query
353
+ order = params[:order] if params.has_key? :order
354
+ end
355
+ reply = []
356
+ resp = @site["search/ticket/?query=#{URI.escape(query)}&orderby=#{order}&format=s"].get
357
+ raise "Invalid query (#{query})" if resp =~ /Invalid query/
358
+ resp = resp.split("\n") # convert to array of lines
359
+ resp.each do |line|
360
+ f = line.match(/^(\d+):\s*(.*)/)
361
+ reply.push [f[1],f[2]] if f.class == MatchData
362
+ end
363
+ reply
364
+ end
365
+
366
+ # A more extensive(expensive) query then the list method. Takes the same
367
+ # parameters as the list method; a string Ticket-SQL query and optional
368
+ # order, but returns a lot more information. Instead of just the ID and
369
+ # subject, you get back an array of hashes, where each hash represents
370
+ # one ticket, indentical to what you get from the show method (which only
371
+ # acts on one ticket). Use with caution; this can take a long time to
372
+ # execute.
373
+ #
374
+ # array = rt.query("Queue='Sales'")
375
+ # array = rt.query(:query => "Queue='Sales'",:order => "+Id")
376
+ # array = rt.query("Queue='Sales'","+Id")
377
+ # => array[0] = { "id" => "123", "requestors" => "someone@..", etc etc }
378
+ # => array[1] = { "id" => "126", "requestors" => "someone@else..", etc etc }
379
+ # => array[0]["id"] = "123"
380
+ def query(*params)
381
+ query = params[0]
382
+ order = ""
383
+ if params.size > 1
384
+ order = params[1]
385
+ end
386
+ if params[0].class == Hash
387
+ params = params[0]
388
+ query = params[:query] if params.has_key? :query
389
+ order = params[:order] if params.has_key? :order
390
+ end
391
+ replies = []
392
+ resp = @site["search/ticket/?query=#{URI.escape(query)}&orderby=#{order}&format=l"].get
393
+ return replies if resp =~/No matching results./
394
+ raise "Invalid query (#{query})" if resp =~ /Invalid query/
395
+ resp.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n\n/,"") # strip HTTP response
396
+ tickets = resp.split("\n--\n") # -- occurs between each ticket
397
+ tickets.each do |ticket|
398
+ ticket_h = response_to_h(ticket)
399
+ reply = {}
400
+ ticket_h.each do |k,v|
401
+ case k
402
+ when 'created','due','told','lastupdated','started'
403
+ begin
404
+ vv = DateTime.parse(v.to_s)
405
+ reply["#{k}"] = vv.strftime("%Y-%m-%d %H:%M:%S")
406
+ rescue ArgumentError
407
+ reply["#{k}"] = v.to_s
408
+ end
409
+ else
410
+ reply["#{k}"] = v.to_s
411
+ end
412
+ end
413
+ replies.push reply
414
+ end
415
+ replies
416
+ end
417
+
418
+ # Get a list of history transactions for a ticket. Takes a ticket ID and
458
419
  # an optional format parameter. If the format is ommitted, the short
459
420
  # format is assumed. If the short format is requested, it returns an
460
421
  # array of 2 element arrays, where each 2-element array is [ticket_id,
@@ -496,6 +457,11 @@ class RT_Client
496
457
  id = id.to_s
497
458
  id = $~[1] if id =~ /ticket\/(\d+)/
498
459
  resp = @site["ticket/#{id}/history?format=#{format[0,1]}"].get
460
+ resp.gsub!(0x1C.chr,'[FS]')
461
+ resp.gsub!(0x1D.chr,'[GS]')
462
+ resp.gsub!(0x1E.chr,'[RS]')
463
+ resp.gsub!(0x1F.chr,'[US]')
464
+
499
465
  if format[0,1] == "s"
500
466
  if comments
501
467
  h = resp.split("\n").select{ |l| l =~ /^\d+:/ }
@@ -510,13 +476,14 @@ class RT_Client
510
476
  while resp.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/) #replace CF spaces with underscores
511
477
  resp.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
512
478
  end
479
+
513
480
  items = resp.split("\n--\n")
514
481
  list = []
515
482
  items.each do |item|
516
- th = TMail::Mail.parse(item)
483
+ th = response_to_h(item)
517
484
  next if not comments and th["type"].to_s =~ /Comment/ # skip comments
518
485
  reply = {}
519
- th.each_header do |k,v|
486
+ th.each do |k,v|
520
487
  attachments = []
521
488
  case k
522
489
  when "attachments"
@@ -540,7 +507,7 @@ class RT_Client
540
507
  end
541
508
  when "content"
542
509
  reply["content"] = v.to_s
543
- temp = item.match(/^Content: (.*?)^\w+:/m) # TMail strips line breaks
510
+ temp = item.match(/^Content: (.*?)^\w+:/m)
544
511
  reply["content"] = temp[1] if temp.class != NilClass
545
512
  else
546
513
  reply["#{k}"] = v.to_s
@@ -571,7 +538,6 @@ class RT_Client
571
538
  id = params[:id] if params.has_key? :id
572
539
  history = params[:history] if params.has_key? :history
573
540
  end
574
- reply = {}
575
541
  resp = @site["ticket/#{id}/history/id/#{history}"].get
576
542
  return reply if resp =~ /not related/ # history id must be related to the ticket id
577
543
  resp.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n\n/,"") # toss the HTTP response
@@ -580,9 +546,9 @@ class RT_Client
580
546
  while resp.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/) #replace CF spaces with underscores
581
547
  resp.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
582
548
  end
583
- th = TMail::Mail.parse(resp)
549
+ th = response_to_h(resp)
584
550
  attachments = []
585
- th.each_header do |k,v|
551
+ th.each do |k,v|
586
552
  case k
587
553
  when "attachments"
588
554
  temp = resp.match(/Attachments:\s*(.*)[^\w|$]/m)
@@ -605,7 +571,7 @@ class RT_Client
605
571
  end
606
572
  when "content"
607
573
  reply["content"] = v.to_s
608
- temp = resp.match(/^Content: (.*?)^\w+:/m) # TMail strips line breaks
574
+ temp = resp.match(/^Content: (.*?)^\w+:/m)
609
575
  reply["content"] = temp[1] if temp.class != NilClass
610
576
  else
611
577
  reply["#{k}"] = v.to_s
@@ -635,7 +601,7 @@ class RT_Client
635
601
  while resp.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/) #replace CF spaces with underscores
636
602
  resp.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
637
603
  end
638
- th = TMail::Mail.parse(resp)
604
+ th = response_to_h(resp)
639
605
  list = []
640
606
  pattern = /(\d+:\s.*?\)),/
641
607
  match = pattern.match(th['attachments'].to_s)
@@ -694,9 +660,9 @@ class RT_Client
694
660
  while resp.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/) #replace CF spaces with underscores
695
661
  resp.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
696
662
  end
697
- headers = TMail::Mail.parse(resp)
663
+ headers = response_to_h(resp)
698
664
  reply = {}
699
- headers.each_header do |k,v|
665
+ headers.each do |k,v|
700
666
  reply["#{k}"] = v.to_s
701
667
  end
702
668
  content = resp.match(/Content:\s+(.*)/m)[1]
@@ -765,63 +731,84 @@ class RT_Client
765
731
  end
766
732
  end
767
733
 
768
- # don't give up the password when the object is inspected
769
- def inspect # :nodoc:
770
- mystr = super()
771
- mystr.gsub!(/(.)pass=.*?([,\}])/,"\\1pass=<hidden>\\2")
772
- mystr
773
- end
774
-
775
- private
776
-
777
- # Private helper for composing RT's "forms". Requires a hash where the
778
- # keys are field names for an RT form. If there's a :Text key, the value
779
- # is modified to insert whitespace on continuation lines. If there's an
780
- # :Attachment key, the value is assumed to be a comma-separated list of
781
- # filenames to attach. It returns a multipart MIME body complete
782
- # with boundaries and headers, suitable for an HTTP POST.
783
- def compose(fields) # :doc:
784
- body = ""
785
- if fields.class != Hash
786
- raise "RT_Client.compose requires parameters as a hash."
787
- end
788
-
789
- # fixup Text field for RFC822 compliance
790
- if fields.has_key? :Text
791
- fields[:Text].gsub!(/\n/,"\n ") # insert a space on continuation lines.
792
- end
793
734
 
794
- # attachments
795
- if fields.has_key? :Attachments
796
- fields[:Attachment] = fields[:Attachments]
797
- fields.delete :Attachments
798
- end
799
- if fields.has_key? :Attachment
800
- filenames = fields[:Attachment].split(',')
801
- i = 0
802
- filenames.each do |v|
803
- filename = File.basename(v)
804
- mime_type = MIME::Types.type_for(v)[0]
805
- i += 1
806
- param_name = "attachment_#{i.to_s}"
807
- body << "--#{@boundary}\r\n"
808
- body << "Content-Disposition: form-data; "
809
- body << "name=\"#{URI.escape(param_name.to_s)}\"; "
810
- body << "filename=\"#{URI.escape(filename)}\"\r\n"
811
- body << "Content-Type: #{mime_type.simplified}\r\n\r\n"
812
- body << File.read(v) # oh dear, lets hope you have lots of RAM
813
- end
814
- # strip paths from filenames
815
- fields[:Attachment] = filenames.map {|f| File.basename(f)}.join(',')
816
- end
817
- field_array = fields.map { |k,v| "#{k}: #{v}" }
818
- content = field_array.join("\n") # our form
819
- # add the form to the end of any attachments
820
- body << "--#{@boundary}\r\n"
821
- body << "Content-Disposition: form-data; "
822
- body << "name=\"content\";\r\n\r\n"
823
- body << content << "\r\n"
824
- body << "--#{@boundary}--\r\n"
825
- body
826
- end
735
+ # don't give up the password when the object is inspected
736
+ def inspect # :nodoc:
737
+ mystr = super()
738
+ mystr.gsub!(/(.)pass=.*?([,\}])/,"\\1pass=<hidden>\\2")
739
+ mystr
740
+ end
741
+
742
+ # helper to convert responses from RT REST to a hash
743
+ def response_to_h(resp)
744
+ resp.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n\n/,"") # toss the HTTP response
745
+ #resp.gsub!(/\n\n/,"\n") # remove double spacing, TMail stops at a blank line
746
+
747
+ # unfold folded fields
748
+ # A newline followed by one or more spaces is treated as a
749
+ # single space
750
+ resp.gsub!(/\n +/, " ")
751
+
752
+ #replace CF spaces with underscores
753
+ while resp.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/)
754
+ resp.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
755
+ end
756
+ return {:error => resp } if resp =~ /does not exist./
757
+
758
+ # convert fields to key value pairs
759
+ ret = {}
760
+ resp.each_line do |ln|
761
+ next unless ln =~ /^.+?:/
762
+ ln_a = ln.split(/:/,2)
763
+ ln_a.map! {|item| item.strip}
764
+ ln_a[0].downcase!
765
+ ret[ln_a[0]] = ln_a[1]
766
+ end
767
+
768
+ return ret
769
+ end
770
+
771
+ # Helper for composing RT's "forms". Requires a hash where the
772
+ # keys are field names for an RT form. If there's a :Text key, the value
773
+ # is modified to insert whitespace on continuation lines. If there's an
774
+ # :Attachment key, the value is assumed to be a comma-separated list of
775
+ # filenames to attach. It returns a hash to be used with rest-client's
776
+ # payload class
777
+ def compose(fields) # :doc:
778
+ if fields.class != Hash
779
+ raise "RT_Client.compose requires parameters as a hash."
780
+ end
781
+
782
+ payload = { :multipart => true }
783
+
784
+ # attachments
785
+ if fields.has_key? :Attachments
786
+ fields[:Attachment] = fields[:Attachments]
787
+ fields.delete :Attachments
788
+ end
789
+ if fields.has_key? :Attachment
790
+ filenames = fields[:Attachment].split(',')
791
+ attachment_num = 1
792
+ filenames.each do |f|
793
+ payload["attachment_#{attachment_num.to_s}"] = File.new(f)
794
+ attachment_num += 1
795
+ end
796
+ # strip paths from filenames
797
+ fields[:Attachment] = filenames.map {|f| File.basename(f)}.join(',')
798
+ end
799
+
800
+ # fixup Text field for RFC822 compliance
801
+ if fields.has_key? :Text
802
+ fields[:Text].gsub!(/\n/,"\n ") # insert a space on continuation lines.
803
+ end
804
+
805
+ field_array = fields.map { |k,v| "#{k}: #{v}" }
806
+ content = field_array.join("\n") # our form
807
+ payload["content"] = content
808
+
809
+ return payload
810
+ end
811
+
827
812
  end
813
+
814
+
data/rt/rtxmlsrv.rb CHANGED
@@ -8,7 +8,6 @@ require "rubygems" # so we can load gems
8
8
  require "rt/client" # rt-client REST library
9
9
  require "xmlrpc/server" # that's what we're doing
10
10
  require "date" # for parsing arbitrary date formats
11
- require "pp"
12
11
 
13
12
  PORT=8080
14
13
  MAX_CONN=50
metadata CHANGED
@@ -1,13 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rt-client
3
3
  version: !ruby/object:Gem::Version
4
- hash: 11
5
- prerelease: false
6
- segments:
7
- - 0
8
- - 5
9
- - 0
10
- version: 0.5.0
4
+ prerelease:
5
+ version: 0.6.4
11
6
  platform: ruby
12
7
  authors:
13
8
  - Tom Lahti
@@ -15,8 +10,7 @@ autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
12
 
18
- date: 2013-05-09 00:00:00 -07:00
19
- default_executable:
13
+ date: 2013-07-31 00:00:00 Z
20
14
  dependencies:
21
15
  - !ruby/object:Gem::Dependency
22
16
  name: rest-client
@@ -26,105 +20,9 @@ dependencies:
26
20
  requirements:
27
21
  - - ">="
28
22
  - !ruby/object:Gem::Version
29
- hash: 25
30
- segments:
31
- - 0
32
- - 9
33
23
  version: "0.9"
34
24
  type: :runtime
35
25
  version_requirements: *id001
36
- - !ruby/object:Gem::Dependency
37
- name: tmail
38
- prerelease: false
39
- requirement: &id002 !ruby/object:Gem::Requirement
40
- none: false
41
- requirements:
42
- - - ">="
43
- - !ruby/object:Gem::Version
44
- hash: 31
45
- segments:
46
- - 1
47
- - 2
48
- - 0
49
- version: 1.2.0
50
- type: :runtime
51
- version_requirements: *id002
52
- - !ruby/object:Gem::Dependency
53
- name: mime-types
54
- prerelease: false
55
- requirement: &id003 !ruby/object:Gem::Requirement
56
- none: false
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- hash: 47
61
- segments:
62
- - 1
63
- - 16
64
- version: "1.16"
65
- type: :runtime
66
- version_requirements: *id003
67
- - !ruby/object:Gem::Dependency
68
- name: archive-tar-minitar
69
- prerelease: false
70
- requirement: &id004 !ruby/object:Gem::Requirement
71
- none: false
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- hash: 1
76
- segments:
77
- - 0
78
- - 5
79
- version: "0.5"
80
- type: :runtime
81
- version_requirements: *id004
82
- - !ruby/object:Gem::Dependency
83
- name: nokogiri
84
- prerelease: false
85
- requirement: &id005 !ruby/object:Gem::Requirement
86
- none: false
87
- requirements:
88
- - - ">="
89
- - !ruby/object:Gem::Version
90
- hash: 11
91
- segments:
92
- - 1
93
- - 2
94
- version: "1.2"
95
- type: :runtime
96
- version_requirements: *id005
97
- - !ruby/object:Gem::Dependency
98
- name: hoe
99
- prerelease: false
100
- requirement: &id006 !ruby/object:Gem::Requirement
101
- none: false
102
- requirements:
103
- - - ">="
104
- - !ruby/object:Gem::Version
105
- hash: 51
106
- segments:
107
- - 1
108
- - 9
109
- - 0
110
- version: 1.9.0
111
- type: :runtime
112
- version_requirements: *id006
113
- - !ruby/object:Gem::Dependency
114
- name: simplecov
115
- prerelease: false
116
- requirement: &id007 !ruby/object:Gem::Requirement
117
- none: false
118
- requirements:
119
- - - ">="
120
- - !ruby/object:Gem::Version
121
- hash: 5
122
- segments:
123
- - 0
124
- - 7
125
- version: "0.7"
126
- type: :runtime
127
- version_requirements: *id007
128
26
  description: " RT_Client is a ruby object that accesses the REST interface version 1.0\n of a Request Tracker instance. See http://www.bestpractical.com/ for\n Request Tracker.\n"
129
27
  email: tlahti@dmsolutions.com
130
28
  executables: []
@@ -136,7 +34,6 @@ extra_rdoc_files: []
136
34
  files:
137
35
  - rt/client.rb
138
36
  - rt/rtxmlsrv.rb
139
- has_rdoc: true
140
37
  homepage:
141
38
  licenses: []
142
39
 
@@ -152,25 +49,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
152
49
  requirements:
153
50
  - - ">="
154
51
  - !ruby/object:Gem::Version
155
- hash: 59
156
- segments:
157
- - 1
158
- - 8
159
- - 6
160
52
  version: 1.8.6
161
53
  required_rubygems_version: !ruby/object:Gem::Requirement
162
54
  none: false
163
55
  requirements:
164
56
  - - ">="
165
57
  - !ruby/object:Gem::Version
166
- hash: 3
167
- segments:
168
- - 0
169
58
  version: "0"
170
59
  requirements:
171
60
  - A working installation of RT with the REST 1.0 interface
172
61
  rubyforge_project:
173
- rubygems_version: 1.3.7
62
+ rubygems_version: 1.8.24
174
63
  signing_key:
175
64
  specification_version: 3
176
65
  summary: Ruby object for RT access via REST