roust 1.0.0

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