rt-client 0.6.8 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
data/rt_client.rb ADDED
@@ -0,0 +1,835 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require "rubygems"
4
+ require "rest_client"
5
+
6
+ ## Author:: Tom Lahti
7
+ ## Copyright:: Copyright (c) 2013 Tom Lahti
8
+ ## License:: Apache 2.0
9
+
10
+ #{<img src="https://badge.fury.io/rb/rt-client.png" alt="Gem Version" />}[http://badge.fury.io/rb/rt-client]
11
+ ##A ruby library API to Request Tracker's REST interface. Requires the
12
+ ##rubygem rest-client 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.
16
+ ##
17
+ ##Thanks to Brian McArdle for patch dealing with spaces in Custom Fields.
18
+ ##To reference custom fields in RT that have spaces with rt-client, use an
19
+ ##underscore in the rt-client code, e.g. "CF.{{Has_Space}}"
20
+ ##
21
+ ##Thanks to Robert Vinson for 1.9.x compatibility when I couldn't be buggered
22
+ ##and a great job of refactoring the old mess into something with much fewer
23
+ ##dependencies.
24
+ ##
25
+ ##Thanks to Steven Craig for a bug fix and feature additions to the
26
+ ##usersearch() method.
27
+ ##
28
+ ##
29
+ ##TODO: Streaming, chunking attachments in compose method
30
+ #
31
+ # See each method for sample usage. To use this, "gem install rt-client" and
32
+ #
33
+ # require "rt/client"
34
+
35
+ class RT_Client
36
+
37
+ UA = "Mozilla/5.0 ruby RT Client Interface 0.7.1"
38
+ attr_reader :status, :site, :version, :cookies, :server, :user, :cookie
39
+
40
+ # Create a new RT_Client object. Load up our stored cookie and check it.
41
+ # Log into RT again if needed and store the new cookie. You can specify
42
+ # login and cookie storage directories in 3 different ways:
43
+ # 1. Explicity during object creation
44
+ # 2. From a .rtclientrc file in the working directory of your ruby program
45
+ # 3. From a .rtclientrc file in the same directory as the library itself
46
+ #
47
+ # These are listed in order of priority; if you have explicit parameters,
48
+ # they are always used, even if you have .rtclientrc files. If there
49
+ # is both an .rtclientrc in your program's working directory and
50
+ # in the library directory, the one from your program's working directory
51
+ # is used. If no parameters are specified either explicity or by use
52
+ # of a .rtclientrc, then the defaults of "rt_user", "rt_pass" are used
53
+ # with a default server of "http://localhost", and cookies are stored
54
+ # in the directory where the library resides.
55
+ #
56
+ # rt= RT_Client.new( :server => "https://tickets.ambulance.com/",
57
+ # :user => "rt_user",
58
+ # :pass => "rt_pass",
59
+ # :cookies => "/my/cookie/dir" )
60
+ #
61
+ # rt= RT_Client.new # use defaults from .rtclientrc
62
+ #
63
+ # .rtclientrc format:
64
+ # server=<RT server>
65
+ # user=<RT user>
66
+ # pass=<RT password>
67
+ # cookies=<directory>
68
+ def initialize(*params)
69
+ @boundary = "----xYzZY#{rand(1000000).to_s}xYzZY"
70
+ @version = "0.7.1"
71
+ @status = "Not connected"
72
+ @server = "http://localhost/"
73
+ @user = "rt_user"
74
+ @pass = "rt_pass"
75
+ @cookies = Dir.pwd
76
+ config_file = Dir.pwd + "/.rtclientrc"
77
+ config = ""
78
+ if File.file?(config_file)
79
+ config = File.read(config_file)
80
+ else
81
+ config_file = File.dirname(__FILE__) + "/.rtclientrc"
82
+ config = File.read(config_file) if File.file?(config_file)
83
+ end
84
+ @server = $~[1] if config =~ /\s*server\s*=\s*(.*)$/i
85
+ @user = $~[1] if config =~ /^\s*user\s*=\s*(.*)$/i
86
+ @pass = $~[1] if config =~ /^\s*pass\s*=\s*(.*)$/i
87
+ @cookies = $~[1] if config =~ /\s*cookies\s*=\s*(.*)$/i
88
+ @resource = "#{@server}REST/1.0/"
89
+ if params.class == Array && params[0].class == Hash
90
+ param = params[0]
91
+ @user = param[:user] if param.has_key? :user
92
+ @pass = param[:pass] if param.has_key? :pass
93
+ if param.has_key? :server
94
+ @server = param[:server]
95
+ @server += "/" if @server !~ /\/$/
96
+ @resource = "#{@server}REST/1.0/"
97
+ end
98
+ @cookies = param[:cookies] if param.has_key? :cookies
99
+ end
100
+ @login = { :user => @user, :pass => @pass }
101
+ cookiejar = "#{@cookies}/RT_Client.#{@user}.cookie" # cookie location
102
+ cookiejar.untaint
103
+ if File.file? cookiejar
104
+ @cookie = File.read(cookiejar).chomp
105
+ headers = { 'User-Agent' => UA,
106
+ 'Content-Type' => "application/x-www-form-urlencoded",
107
+ 'Cookie' => @cookie }
108
+ else
109
+ headers = { 'User-Agent' => UA,
110
+ 'Content-Type' => "application/x-www-form-urlencoded" }
111
+ @cookie = ""
112
+ end
113
+
114
+
115
+ site = RestClient::Resource.new(@resource, :headers => headers, :timeout => 240)
116
+ data = site.post "" # a null post just to check that we are logged in
117
+
118
+ if @cookie.length == 0 or data =~ /401/ # we're not logged in
119
+ data = site.post @login, :headers => headers
120
+ # puts data
121
+ @cookie = data.headers[:set_cookie].first.split('; ')[0]
122
+ # write the new cookie
123
+ if @cookie !~ /nil/
124
+ f = File.new(cookiejar,"w")
125
+ f.puts @cookie
126
+ f.close
127
+ end
128
+ end
129
+ headers = { 'User-Agent' => UA,
130
+ 'Content-Type' => "multipart/form-data; boundary=#{@boundary}",
131
+ 'Cookie' => @cookie }
132
+ @site = RestClient::Resource.new(@resource, :headers => headers)
133
+ @status = data
134
+ self.untaint
135
+ end
136
+
137
+ # gets the detail for a single ticket/user. If its a ticket, its without
138
+ # history or attachments (to get those use the history method) . If no
139
+ # type is specified, ticket is assumed. takes a single parameter
140
+ # containing the ticket/user id, and returns a hash of RT Fields => values
141
+ #
142
+ # hash = rt.show(822)
143
+ # hash = rt.show("822")
144
+ # hash = rt.show("ticket/822")
145
+ # hash = rt.show(:id => 822)
146
+ # hash = rt.show(:id => "822")
147
+ # hash = rt.show(:id => "ticket/822")
148
+ # hash = rt.show("user/#{login}")
149
+ # email = rt.show("user/somebody")["emailaddress"]
150
+ def show(id)
151
+ id = id[:id] if id.class == Hash
152
+ id = id.to_s
153
+ type = "ticket"
154
+ sid = id
155
+ if id =~ /(\w+)\/(.+)/
156
+ type = $~[1]
157
+ sid = $~[2]
158
+ end
159
+ reply = {}
160
+ if type.downcase == 'user'
161
+ resp = @site["#{type}/#{sid}"].get
162
+ else
163
+ resp = @site["#{type}/#{sid}/show"].get
164
+ end
165
+ reply = response_to_h(resp)
166
+ end
167
+
168
+ # gets a list of ticket links for a ticket.
169
+ # takes a single parameter containing the ticket id,
170
+ # and returns a hash of RT Fields => values
171
+ #
172
+ # hash = rt.links(822)
173
+ # hash = rt.links("822")
174
+ # hash = rt.links("ticket/822")
175
+ # hash = rt.links(:id => 822)
176
+ # hash = rt.links(:id => "822")
177
+ # hash = rt.links(:id => "ticket/822")
178
+ def links(id)
179
+ id = id[:id] if id.class == Hash
180
+ id = id.to_s
181
+ type = "ticket"
182
+ sid = id
183
+ if id =~ /(\w+)\/(.+)/
184
+ type = $~[1]
185
+ sid = $~[2]
186
+ end
187
+ reply = {}
188
+ resp = @site["ticket/#{sid}/links/show"].get
189
+ return {:error => resp, } if resp =~ /does not exist./
190
+ reply = response_to_h(resp)
191
+ end
192
+
193
+ # Creates a new ticket. Requires a hash that contains RT form fields as
194
+ # the keys. Capitalization is important; use :Queue, not :queue. You
195
+ # will need at least :Queue to create a ticket. For a full list of fields
196
+ # you can use, try "/opt/rt3/bin/rt edit ticket/1". Returns the newly
197
+ # created ticket number, or a complete REST response.
198
+ #
199
+ # id = rt.create( :Queue => "Customer Service",
200
+ # :Cc => "somebody\@email.com",
201
+ # :Subject => "I've fallen and I can't get up",
202
+ # :Text => "I think my hip is broken.\nPlease help.",
203
+ # :"CF.{CustomField}" => "Urgent",
204
+ def create(field_hash)
205
+ field_hash[:id] = "ticket/new"
206
+ payload = compose(field_hash)
207
+ # puts "Payload for new ticket:"
208
+ # puts payload
209
+ resp = @site['ticket/new/edit'].post payload
210
+ new_id = resp.match(/Ticket\s*(\d+)/)
211
+ if new_id.class == MatchData
212
+ new_ticket = new_id[1]
213
+ else
214
+ new_ticket = resp
215
+ end
216
+ new_ticket # return the ticket number, or the full REST response
217
+ end
218
+
219
+ # create a new user. Requires a hash of RT fields => values. Returns
220
+ # the newly created user ID, or the full REST response if there is an error.
221
+ # For a full list of possible parameters that you can specify, look at
222
+ # "/opt/rt/bin/rt edit user/1"
223
+ #
224
+ # new_id = rt.create_user(:Name => "Joe_Smith", :EmailAddress => "joes\@here.com")
225
+ def create_user(field_hash)
226
+ field_hash[:id] = "user/new"
227
+ payload = compose(field_hash)
228
+ resp = @site['user/new/edit'].post payload
229
+ new_id = resp.match(/User\s*(\d+)/)
230
+ if new_id.class == MatchData
231
+ new_user = new_id[1]
232
+ else
233
+ new_user = resp
234
+ end
235
+ new_user # return the new user id or the full REST response
236
+ end
237
+
238
+ # edit or create a user. If the user exists, edits the user as "edit" would.
239
+ # If the user doesn't exist, creates it as "create_user" would.
240
+ def edit_or_create_user(field_hash)
241
+ if field_hash.has_key? :id
242
+ id = field_hash[:id]
243
+ if id !~ /^user\//
244
+ id = "user/#{id}"
245
+ field_hash[:id] = id
246
+ end
247
+ else
248
+ raise "RT_Client.edit_or_create_user require a user id in the 'id' key."
249
+ end
250
+ resp1 = "not called"
251
+ resp2 = "not called"
252
+ resp1 = edit(field_hash)
253
+ resp2 = create_user(field_hash) if resp1 =~ /does not exist./
254
+ resp = "Edit: #{resp1}\nCreate:#{resp2}"
255
+ resp
256
+ end
257
+
258
+ # edit an existing ticket/user. Requires a hash containing RT
259
+ # form fields as keys. the key :id is required.
260
+ # returns the complete REST response, whatever it is. If the
261
+ # id supplied contains "user/", it edits a user, otherwise
262
+ # it edits a ticket. For a full list of fields you can edit,
263
+ # try "/opt/rt3/bin/rt edit ticket/1"
264
+ #
265
+ # resp = rt.edit(:id => 822, :Status => "resolved")
266
+ # resp = rt.edit(:id => ticket_id, :"CF.{CustomField}" => var)
267
+ # resp = rt.edit(:id => "user/someone", :EMailAddress => "something@here.com")
268
+ # resp = rt.edit(:id => "user/bossman", :Password => "mypass")
269
+ # resp = rt.edit(:id => "user/4306", :Disabled => "1")
270
+ def edit(field_hash)
271
+ if field_hash.has_key? :id
272
+ id = field_hash[:id]
273
+ else
274
+ raise "RT_Client.edit requires a user or ticket id in the 'id' key."
275
+ end
276
+ type = "ticket"
277
+ sid = id
278
+ if id =~ /(\w+)\/(.+)/
279
+ type = $~[1]
280
+ sid = $~[2]
281
+ end
282
+ payload = compose(field_hash)
283
+ resp = @site["#{type}/#{sid}/edit"].post payload
284
+ resp
285
+ end
286
+
287
+ # Comment on a ticket. Requires a hash, which must have an :id key
288
+ # containing the ticket number. Returns the REST response. For a list of
289
+ # fields you can use in a comment, try "/opt/rt3/bin/rt comment ticket/1"
290
+ #
291
+ # rt.comment( :id => id,
292
+ # :Text => "Oh dear, I wonder if the hip smells like almonds?",
293
+ # :Attachment => "/tmp/almonds.gif" )
294
+ def comment(field_hash)
295
+ if field_hash.has_key? :id
296
+ id = field_hash[:id]
297
+ else
298
+ raise "RT_Client.comment requires a Ticket number in the 'id' key."
299
+ end
300
+ field_hash[:Action] = "comment"
301
+ payload = compose(field_hash)
302
+ @site["ticket/#{id}/comment"].post payload
303
+ end
304
+
305
+ # Find RT user details from an email address or a name
306
+ #
307
+ # rt.usersearch(:EmailAddress => 'some@email.com')
308
+ # rt.usersearch(:Name => username)
309
+ # -> {"name"=>"rtlogin", "realname"=>"John Smith", "address1"=>"123 Main", etc }
310
+ def usersearch(field_hash)
311
+ key = nil
312
+ key = field_hash[:EmailAddress] if field_hash.has_key? :EmailAddress
313
+ key = field_hash[:Name] if field_hash.has_key? :Name
314
+ if key == nil
315
+ raise "RT_Client.usersearch requires an RT user email in the 'EmailAddress' key or an RT username in the 'Name' key"
316
+ end
317
+ reply = {}
318
+ resp = @site["user/#{key}"].get
319
+ return reply if resp =~ /No user named/
320
+ reply = response_to_h(resp)
321
+ end
322
+
323
+ # An alias for usersearch()
324
+
325
+ def usernamesearch(field_hash)
326
+ usersearch(field_hash)
327
+ end
328
+
329
+ # correspond on a ticket. Requires a hash, which must have an :id key
330
+ # containing the ticket number. Returns the REST response. For a list of
331
+ # fields you can use in correspondence, try "/opt/rt3/bin/rt correspond
332
+ # ticket/1"
333
+ #
334
+ # rt.correspond( :id => id,
335
+ # :Text => "We're sending help right away.",
336
+ # :Attachment => "/tmp/admittance.doc" )
337
+ def correspond(field_hash)
338
+ if field_hash.has_key? :id
339
+ if field_hash[:id] =~ /ticket\/(\d+)/
340
+ id = $~[1]
341
+ else
342
+ id = field_hash[:id]
343
+ end
344
+ else
345
+ raise "RT_Client.correspond requires a Ticket number in the 'id' key."
346
+ end
347
+ field_hash[:Action] = "correspond"
348
+ payload = compose(field_hash)
349
+ @site["ticket/#{id}/comment"].post payload
350
+ end
351
+
352
+ # Get a list of tickets matching some criteria.
353
+ # Takes a string Ticket-SQL query and an optional "order by" parameter.
354
+ # The order by is an RT field, prefix it with + for ascending
355
+ # or - for descending.
356
+ # Returns a nested array of arrays containing [ticket number, subject]
357
+ # The outer array is in the order requested.
358
+ #
359
+ # hash = rt.list(:query => "Queue = 'Sales'")
360
+ # hash = rt.list("Queue='Sales'")
361
+ # hash = rt.list(:query => "Queue = 'Sales'", :order => "-Id")
362
+ # hash = rt.list("Queue='Sales'","-Id")
363
+ def list(*params)
364
+ query = params[0]
365
+ order = ""
366
+ if params.size > 1
367
+ order = params[1]
368
+ end
369
+ if params[0].class == Hash
370
+ params = params[0]
371
+ query = params[:query] if params.has_key? :query
372
+ order = params[:order] if params.has_key? :order
373
+ end
374
+ reply = []
375
+ resp = @site["search/ticket/?query=#{URI.escape(query)}&orderby=#{order}&format=s"].get
376
+ raise "Invalid query (#{query})" if resp =~ /Invalid query/
377
+ resp = resp.split("\n") # convert to array of lines
378
+ resp.each do |line|
379
+ f = line.match(/^(\d+):\s*(.*)/)
380
+ reply.push [f[1],f[2]] if f.class == MatchData
381
+ end
382
+ reply
383
+ end
384
+
385
+ # A more extensive(expensive) query then the list method. Takes the same
386
+ # parameters as the list method; a string Ticket-SQL query and optional
387
+ # order, but returns a lot more information. Instead of just the ID and
388
+ # subject, you get back an array of hashes, where each hash represents
389
+ # one ticket, indentical to what you get from the show method (which only
390
+ # acts on one ticket). Use with caution; this can take a long time to
391
+ # execute.
392
+ #
393
+ # array = rt.query("Queue='Sales'")
394
+ # array = rt.query(:query => "Queue='Sales'",:order => "+Id")
395
+ # array = rt.query("Queue='Sales'","+Id")
396
+ # => array[0] = { "id" => "123", "requestors" => "someone@..", etc etc }
397
+ # => array[1] = { "id" => "126", "requestors" => "someone@else..", etc etc }
398
+ # => array[0]["id"] = "123"
399
+ def query(*params)
400
+ query = params[0]
401
+ order = ""
402
+ if params.size > 1
403
+ order = params[1]
404
+ end
405
+ if params[0].class == Hash
406
+ params = params[0]
407
+ query = params[:query] if params.has_key? :query
408
+ order = params[:order] if params.has_key? :order
409
+ end
410
+ replies = []
411
+ resp = @site["search/ticket/?query=#{URI.escape(query)}&orderby=#{order}&format=l"].get
412
+ return replies if resp =~/No matching results./
413
+ raise "Invalid query (#{query})" if resp =~ /Invalid query/
414
+ resp.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n\n/,"") # strip HTTP response
415
+ tickets = resp.split("\n--\n") # -- occurs between each ticket
416
+ tickets.each do |ticket|
417
+ ticket_h = response_to_h(ticket)
418
+ reply = {}
419
+ ticket_h.each do |k,v|
420
+ case k
421
+ when 'created','due','told','lastupdated','started'
422
+ begin
423
+ vv = DateTime.parse(v.to_s)
424
+ reply["#{k}"] = vv.strftime("%Y-%m-%d %H:%M:%S")
425
+ rescue ArgumentError
426
+ reply["#{k}"] = v.to_s
427
+ end
428
+ else
429
+ reply["#{k}"] = v.to_s
430
+ end
431
+ end
432
+ replies.push reply
433
+ end
434
+ replies
435
+ end
436
+
437
+ # Get a list of history transactions for a ticket. Takes a ticket ID and
438
+ # an optional format parameter. If the format is ommitted, the short
439
+ # format is assumed. If the short format is requested, it returns an
440
+ # array of 2 element arrays, where each 2-element array is [ticket_id,
441
+ # description]. If the long format is requested, it returns an array of
442
+ # hashes, where each hash contains the keys:
443
+ #
444
+ # id:: (history-id)
445
+ # Ticket:: (Ticket this history item belongs to)
446
+ # TimeTaken:: (time entered by the actor that generated this item)
447
+ # Type:: (what type of history item this is)
448
+ # Field:: (what field is affected by this item, if any)
449
+ # OldValue:: (the old value of the Field)
450
+ # NewValue:: (the new value of the Field)
451
+ # Data:: (Additional data about the item)
452
+ # Description:: (Description of this item; same as short format)
453
+ # Content:: (The content of this item)
454
+ # Creator:: (the RT user that created this item)
455
+ # Created:: (Date/time this item was created)
456
+ # Attachments:: (a hash describing attachments to this item)
457
+ #
458
+ # history = rt.history(881)
459
+ # history = rt.history(881,"short")
460
+ # => [["10501"," Ticket created by blah"],["10510"," Comments added by userX"]]
461
+ # history = rt.history(881,"long")
462
+ # history = rt.history(:id => 881, :format => "long")
463
+ # => [{"id" => "6171", "ticket" => "881" ....}, {"id" => "6180", ...} ]
464
+ def history(*params)
465
+ id = params[0]
466
+ format = "short"
467
+ format = params[1].downcase if params.size > 1
468
+ comments = false
469
+ comments = params[2] if params.size > 2
470
+ if params[0].class == Hash
471
+ params = params[0]
472
+ id = params[:id] if params.has_key? :id
473
+ format = params[:format].downcase if params.has_key? :format
474
+ comments = params[:comments] if params.has_key? :comments
475
+ end
476
+ id = id.to_s
477
+ id = $~[1] if id =~ /ticket\/(\d+)/
478
+ resp = @site["ticket/#{id}/history?format=#{format[0,1]}"].get
479
+ resp.gsub!(0x1C.chr,'[FS]')
480
+ resp.gsub!(0x1D.chr,'[GS]')
481
+ resp.gsub!(0x1E.chr,'[RS]')
482
+ resp.gsub!(0x1F.chr,'[US]')
483
+
484
+ if format[0,1] == "s"
485
+ if comments
486
+ h = resp.split("\n").select{ |l| l =~ /^\d+:/ }
487
+ else
488
+ h = resp.split("\n").select{ |l| l =~ /^\d+: [^Comments]/ }
489
+ h.select!{ |l| l !~ /Outgoing email about a comment/ }
490
+ end
491
+ list = h.map { |l| l.split(":") }
492
+ else
493
+ resp.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n\n/,"") # toss the HTTP response
494
+ resp.gsub!(/^#.*?\n\n/,"") # toss the 'total" line
495
+ resp.gsub!(/^\n/m,"") # toss blank lines
496
+ while resp.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/) #replace CF spaces with underscores
497
+ resp.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
498
+ end
499
+
500
+ items = resp.split("\n--\n")
501
+ list = []
502
+ items.each do |item|
503
+ th = response_to_h(item)
504
+ next if not comments and th["type"].to_s =~ /Comment/ # skip comments
505
+ reply = {}
506
+ th.each do |k,v|
507
+ attachments = []
508
+ case k
509
+ when "attachments"
510
+ temp = item.match(/Attachments:\s*(.*)/m)
511
+ if temp.class != NilClass
512
+ atarr = temp[1].split("\n")
513
+ atarr.map { |a| a.gsub!(/^\s*/,"") }
514
+ atarr.each do |a|
515
+ i = a.match(/(\d+):\s*(.*)/)
516
+ s={}
517
+ s[:id] = i[1].to_s
518
+ s[:name] = i[2].to_s
519
+ sz = i[2].match(/(.*?)\s*\((.*?)\)/)
520
+ if sz.class == MatchData
521
+ s[:name] = sz[1].to_s
522
+ s[:size] = sz[2].to_s
523
+ end
524
+ attachments.push s
525
+ end
526
+ reply["attachments"] = attachments
527
+ end
528
+ when "content"
529
+ reply["content"] = v.to_s
530
+ temp = item.match(/^Content: (.*?)^\w+:/m)
531
+ reply["content"] = temp[1] if temp.class != NilClass
532
+ else
533
+ reply["#{k}"] = v.to_s
534
+ end
535
+ end
536
+ list.push reply
537
+ end
538
+ end
539
+ list
540
+ end
541
+
542
+ # Get the detail for a single history item. Needs a ticket ID and a
543
+ # history item ID, returns a hash of RT Fields => values. The hash
544
+ # also contains a special key named "attachments", whose value is
545
+ # an array of hashes, where each hash represents an attachment. The hash
546
+ # keys are :id, :name, and :size.
547
+ #
548
+ # x = rt.history_item(21, 6692)
549
+ # x = rt.history_item(:id => 21, :history => 6692)
550
+ # => x = {"ticket" => "21", "creator" => "somebody", "description" =>
551
+ # => "something happened", "attachments" => [{:name=>"file.txt",
552
+ # => :id=>"3289", size=>"651b"}, {:name=>"another.doc"... }]}
553
+ def history_item(*params)
554
+ id = params[0]
555
+ history = params[1]
556
+ if params[0].class == Hash
557
+ params = params[0]
558
+ id = params[:id] if params.has_key? :id
559
+ history = params[:history] if params.has_key? :history
560
+ end
561
+ reply = {}
562
+ resp = @site["ticket/#{id}/history/id/#{history}"].get
563
+ return reply if resp =~ /not related/ # history id must be related to the ticket id
564
+ resp.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n\n/,"") # toss the HTTP response
565
+ resp.gsub!(/^#.*?\n\n/,"") # toss the 'total" line
566
+ resp.gsub!(/^\n/m,"") # toss blank lines
567
+ while resp.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/) #replace CF spaces with underscores
568
+ resp.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
569
+ end
570
+ th = response_to_h(resp)
571
+ attachments = []
572
+ th.each do |k,v|
573
+ case k
574
+ when "attachments"
575
+ temp = resp.match(/Attachments:\s*(.*)[^\w|$]/m)
576
+ if temp.class != NilClass
577
+ atarr = temp[1].split("\n")
578
+ atarr.map { |a| a.gsub!(/^\s*/,"") }
579
+ atarr.each do |a|
580
+ i = a.match(/(\d+):\s*(.*)/)
581
+ s={}
582
+ s[:id] = i[1]
583
+ s[:name] = i[2]
584
+ sz = i[2].match(/(.*?)\s*\((.*?)\)/)
585
+ if sz.class == MatchData
586
+ s[:name] = sz[1]
587
+ s[:size] = sz[2]
588
+ end
589
+ attachments.push s
590
+ end
591
+ reply["#{k}"] = attachments
592
+ end
593
+ when "content"
594
+ reply["content"] = v.to_s
595
+ temp = resp.match(/^Content: (.*?)^\w+:/m)
596
+ reply["content"] = temp[1] if temp.class != NilClass
597
+ else
598
+ reply["#{k}"] = v.to_s
599
+ end
600
+ end
601
+ reply
602
+ end
603
+
604
+ # Get a list of attachments related to a ticket.
605
+ # Requires a ticket id, returns an array of hashes where each hash
606
+ # represents one attachment. Hash keys are :id, :name, :type, :size.
607
+ # You can optionally request that unnamed attachments be included,
608
+ # the default is to not include them.
609
+ def attachments(*params)
610
+ id = params[0]
611
+ unnamed = params[1]
612
+ if params[0].class == Hash
613
+ params = params[0]
614
+ id = params[:id] if params.has_key? :id
615
+ unnamed = params[:unnamed] if params.has_key? :unnamed
616
+ end
617
+ unnamed = false if unnamed.to_s == "0"
618
+ id = $~[1] if id =~ /ticket\/(\d+)/
619
+ resp = @site["ticket/#{id}/attachments"].get
620
+ resp.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n\n/,"") # toss the HTTP response
621
+ resp.gsub!(/^\n/m,"") # toss blank lines
622
+ while resp.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/) #replace CF spaces with underscores
623
+ resp.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
624
+ end
625
+ th = response_to_h(resp)
626
+ list = []
627
+ pattern = /(\d+:\s.*?\)),/
628
+ match = pattern.match(th['attachments'].to_s)
629
+ while match != nil
630
+ list.push match[0]
631
+ s = match.post_match
632
+ match = pattern.match(s)
633
+ end
634
+ attachments = []
635
+ list.each do |v|
636
+ attachment = {}
637
+ m=v.match(/(\d+):\s+(.*?)\s+\((.*?)\s+\/\s+(.*?)\)/)
638
+ if m.class == MatchData
639
+ next if m[2] == "(Unnamed)" and !unnamed
640
+ attachment[:id] = m[1]
641
+ attachment[:name] = m[2]
642
+ attachment[:type] = m[3]
643
+ attachment[:size] = m[4]
644
+ attachments.push attachment
645
+ end
646
+ end
647
+ attachments
648
+ end
649
+
650
+ # Get attachment content for single attachment. Requires a ticket ID
651
+ # and an attachment ID, which must be related. If a directory parameter
652
+ # is supplied, the attachment is written to that directory. If not,
653
+ # the attachment content is returned in the hash returned by the
654
+ # function as the key 'content', along with some other keys you always get:
655
+ #
656
+ # transaction:: the transaction id
657
+ # creator:: the user id number who attached it
658
+ # id:: the attachment id
659
+ # filename:: the name of the file
660
+ # contenttype:: MIME content type of the attachment
661
+ # created:: date of the attachment
662
+ # parent:: an attachment id if this was an embedded MIME attachment
663
+ #
664
+ # x = get_attachment(21,3879)
665
+ # x = get_attachment(:ticket => 21, :attachment => 3879)
666
+ # x = get_attachment(:ticket => 21, :attachment => 3879, :dir = "/some/dir")
667
+ def get_attachment(*params)
668
+ tid = params[0]
669
+ aid = params[1]
670
+ dir = nil
671
+ dir = params[2] if params.size > 2
672
+ if params[0].class == Hash
673
+ params = params[0]
674
+ tid = params[:ticket] if params.has_key? :ticket
675
+ aid = params[:attachment] if params.has_key? :attachment
676
+ dir = params[:dir] if params.has_key? :dir
677
+ end
678
+ tid = $~[1] if tid =~ /ticket\/(\d+)/
679
+ resp = @site["ticket/#{tid}/attachments/#{aid}"].get
680
+ resp.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n\n/,"") # toss HTTP response
681
+ while resp.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/) #replace CF spaces with underscores
682
+ resp.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
683
+ end
684
+ headers = response_to_h(resp)
685
+ reply = {}
686
+ headers.each do |k,v|
687
+ reply["#{k}"] = v.to_s
688
+ end
689
+ content = resp.match(/Content:\s+(.*)/m)[1]
690
+ content.gsub!(/\n\s{9}/,"\n") # strip leading spaces on each line
691
+ content.chomp!
692
+ content.chomp!
693
+ content.chomp! # 3 carriage returns at the end
694
+ binary = Iconv.conv("ISO-8859-1","UTF-8",content) # convert encoding
695
+ if dir
696
+ fh = File.new("#{dir}/#{headers['Filename'].to_s}","wb")
697
+ fh.write binary
698
+ fh.close
699
+ else
700
+ reply["content"] = binary
701
+ end
702
+ reply
703
+ end
704
+
705
+ # Add a watcher to a ticket, but only if not already a watcher. Takes a
706
+ # ticket ID, an email address (or array of email addresses), and an
707
+ # optional watcher type. If no watcher type is specified, its assumed to
708
+ # be "Cc". Possible watcher types are 'Requestors', 'Cc', and 'AdminCc'.
709
+ #
710
+ # rt.add_watcher(123,"someone@here.com")
711
+ # rt.add_watcher(123,["someone@here.com","another@there.com"])
712
+ # rt.add_watcher(123,"someone@here.com","Requestors")
713
+ # rt.add_watcher(:id => 123, :addr => "someone@here.com")
714
+ # rt.add_watcher(:id => 123, :addr => ["someone@here.com","another@there.com"])
715
+ # rt.add_watcher(:id => 123, :addr => "someone@here.com", :type => "AdminCc")
716
+ def add_watcher(*params)
717
+ tid = params[0]
718
+ addr = []
719
+ type = "cc"
720
+ addr = params[1] if params.size > 1
721
+ type = params[2] if params.size > 2
722
+ if params[0].class == Hash
723
+ params = params[0]
724
+ tid = params[:id] if params.has_key? :id
725
+ addr = params[:addr] if params.has_key? :addr
726
+ type = params[:type] if params.has_key? :type
727
+ end
728
+ addr = addr.to_a.uniq # make it array if its just a string, and remove dups
729
+ type.downcase!
730
+ tobj = show(tid) # get current watchers
731
+ ccs = tobj["cc"].split(", ")
732
+ accs = tobj["admincc"].split(", ")
733
+ reqs = tobj["requestors"].split(", ")
734
+ watchers = ccs | accs | reqs # union of all watchers
735
+ addr.each do |e|
736
+ case type
737
+ when "cc"
738
+ ccs.push(e) if not watchers.include?(e)
739
+ when "admincc"
740
+ accs.push(e) if not watchers.include?(e)
741
+ when "requestors"
742
+ reqs.push(e) if not watchers.include?(e)
743
+ end
744
+ end
745
+ case type
746
+ when "cc"
747
+ edit(:id => tid, :Cc => ccs.join(","))
748
+ when "admincc"
749
+ edit(:id => tid, :AdminCc => accs.join(","))
750
+ when "requestors"
751
+ edit(:id => tid, :Requestors => reqs.join(","))
752
+ end
753
+ end
754
+
755
+
756
+ # don't give up the password when the object is inspected
757
+ def inspect # :nodoc:
758
+ mystr = super()
759
+ mystr.gsub!(/(.)pass=.*?([,\}])/,"\\1pass=<hidden>\\2")
760
+ mystr
761
+ end
762
+
763
+ # helper to convert responses from RT REST to a hash
764
+ def response_to_h(resp) # :nodoc:
765
+ resp.gsub!(/RT\/\d+\.\d+\.\d+\s\d{3}\s.*\n\n/,"") # toss the HTTP response
766
+ #resp.gsub!(/\n\n/,"\n") # remove double spacing, TMail stops at a blank line
767
+
768
+ # unfold folded fields
769
+ # A newline followed by one or more spaces is treated as a
770
+ # single space
771
+ resp.gsub!(/\n +/, " ")
772
+
773
+ #replace CF spaces with underscores
774
+ while resp.match(/CF\.\{[\w_ ]*[ ]+[\w ]*\}/)
775
+ resp.gsub!(/CF\.\{([\w_ ]*)([ ]+)([\w ]*)\}/, 'CF.{\1_\3}')
776
+ end
777
+ return {:error => resp } if resp =~ /does not exist./
778
+
779
+ # convert fields to key value pairs
780
+ ret = {}
781
+ resp.each_line do |ln|
782
+ next unless ln =~ /^.+?:/
783
+ ln_a = ln.split(/:/,2)
784
+ ln_a.map! {|item| item.strip}
785
+ ln_a[0].downcase!
786
+ ret[ln_a[0]] = ln_a[1]
787
+ end
788
+
789
+ return ret
790
+ end
791
+
792
+ # Helper for composing RT's "forms". Requires a hash where the
793
+ # keys are field names for an RT form. If there's a :Text key, the value
794
+ # is modified to insert whitespace on continuation lines. If there's an
795
+ # :Attachment key, the value is assumed to be a comma-separated list of
796
+ # filenames to attach. It returns a hash to be used with rest-client's
797
+ # payload class
798
+ def compose(fields) # :nodoc:
799
+ if fields.class != Hash
800
+ raise "RT_Client.compose requires parameters as a hash."
801
+ end
802
+
803
+ payload = { :multipart => true }
804
+
805
+ # attachments
806
+ if fields.has_key? :Attachments
807
+ fields[:Attachment] = fields[:Attachments]
808
+ fields.delete :Attachments
809
+ end
810
+ if fields.has_key? :Attachment
811
+ filenames = fields[:Attachment].split(',')
812
+ attachment_num = 1
813
+ filenames.each do |f|
814
+ payload["attachment_#{attachment_num.to_s}"] = File.new(f)
815
+ attachment_num += 1
816
+ end
817
+ # strip paths from filenames
818
+ fields[:Attachment] = filenames.map {|f| File.basename(f)}.join(',')
819
+ end
820
+
821
+ # fixup Text field for RFC822 compliance
822
+ if fields.has_key? :Text
823
+ fields[:Text].gsub!(/\n/,"\n ") # insert a space on continuation lines.
824
+ end
825
+
826
+ field_array = fields.map { |k,v| "#{k}: #{v}" }
827
+ content = field_array.join("\n") # our form
828
+ payload["content"] = content
829
+
830
+ return payload
831
+ end
832
+
833
+ end
834
+
835
+