rt-client 0.6.8 → 0.7.1

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.
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
+