rt-client 0.5.0 → 0.6.4

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