rt-client 0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/rt/client.rb +699 -0
  2. data/rt/rtxmlsrv.rb +246 -0
  3. metadata +85 -0
data/rt/client.rb ADDED
@@ -0,0 +1,699 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require "rubygems"
4
+ require "rest_client"
5
+ require "tmail"
6
+ require "iconv"
7
+ require 'mime/types' # requires both nokogiri and rcov. Yuck.
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
+ ##TODO: Streaming, chunking attachments in compose method
16
+ #
17
+ # See each method for sample usage. To use this, "gem install rt-client" and
18
+ #
19
+ # require "rt/client"
20
+
21
+ class RT_Client
22
+
23
+ UA = "Mozilla/5.0 ruby RT Client Interface 0.2"
24
+ attr_reader :status, :site, :version, :cookies, :server, :user, :cookie
25
+
26
+ # Create a new RT_Client object. Load up our stored cookie and check it.
27
+ # Log into RT again if needed and store the new cookie. You can specify
28
+ # login and cookie storage directories in 3 different ways:
29
+ # 1. Explicity during object creation
30
+ # 2. From a .rtclientrc file in the working directory of your ruby program
31
+ # 3. From a .rtclientrc file in the same directory as the library itself
32
+ #
33
+ # These are listed in order of priority; if you have explicit parameters,
34
+ # they are always used, even if you have .rtclientrc files. If there
35
+ # is both an .rtclientrc in your program's working directory and
36
+ # in the library directory, the one from your program's working directory
37
+ # is used. If no parameters are specified either explicity or by use
38
+ # of a .rtclientrc, then the defaults of "rt_user", "rt_pass" are used
39
+ # with a default server of "http://localhost", and cookies are stored
40
+ # in the directory where the library resides.
41
+ #
42
+ # rt= RT_Client.new( :server => "https://tickets.ambulance.com/",
43
+ # :user => "rt_user",
44
+ # :pass => "rt_pass",
45
+ # :cookies => "/my/cookie/dir" )
46
+ #
47
+ # rt= RT_Client.new # use defaults from .rtclientrc
48
+ #
49
+ # .rtclientrc format:
50
+ # server=<RT server>
51
+ # user=<RT user>
52
+ # pass=<RT password>
53
+ # cookies=<directory>
54
+ def initialize(*params)
55
+ @boundary = "----xYzZY#{rand(1000000).to_s}xYzZY"
56
+ @version = "0.2"
57
+ @status = "Not connected"
58
+ @server = "http://localhost/"
59
+ @user = "rt_user"
60
+ @pass = "rt_pass"
61
+ @cookies = Dir.pwd
62
+ config_file = Dir.pwd + "/.rtclientrc"
63
+ config = ""
64
+ if File.file?(config_file)
65
+ config = File.read(config_file)
66
+ else
67
+ config_file = File.dirname(__FILE__) + "/.rtclientrc"
68
+ config = File.read(config_file) if File.file?(config_file)
69
+ end
70
+ @server = $~[1] if config =~ /\s*server\s*=\s*(.*)$/i
71
+ @user = $~[1] if config =~ /^\s*user\s*=\s*(.*)$/i
72
+ @pass = $~[1] if config =~ /^\s*pass\s*=\s*(.*)$/i
73
+ @cookies = $~[1] if config =~ /\s*cookies\s*=\s*(.*)$/i
74
+ @resource = "#{@server}REST/1.0/"
75
+ if params.class == Array && params[0].class == Hash
76
+ param = params[0]
77
+ @user = param[:user] if param.has_key? :user
78
+ @pass = param[:pass] if param.has_key? :pass
79
+ if param.has_key? :server
80
+ @server = param[:server]
81
+ @server += "/" if @server !~ /\/$/
82
+ @resource = "#{@server}REST/1.0/"
83
+ end
84
+ @cookies = param[:cookies] if param.has_key? :cookies
85
+ end
86
+ @login = { :user => @user, :pass => @pass }
87
+ cookiejar = "#{@cookies}/RT_Client.#{@user}.cookie" # cookie location
88
+ cookiejar.untaint
89
+ if File.file? cookiejar
90
+ @cookie = File.read(cookiejar).chomp
91
+ headers = { 'User-Agent' => UA,
92
+ 'Content-Type' => "application/x-www-form-urlencoded",
93
+ 'Cookie' => @cookie }
94
+ else
95
+ headers = { 'User-Agent' => UA,
96
+ 'Content-Type' => "application/x-www-form-urlencoded" }
97
+ @cookie = ""
98
+ end
99
+
100
+
101
+ site = RestClient::Resource.new(@resource, :headers => headers)
102
+ data = site.post "" # a null post just to check that we are logged in
103
+
104
+ if @cookie.length == 0 or data =~ /401/ # we're not logged in
105
+ data = site.post @login, :headers => headers
106
+ puts data
107
+ @cookie = data.headers[:set_cookie].to_s.split('; ')[0]
108
+ # write the new cookie
109
+ if @cookie !~ /nil/
110
+ f = File.new(cookiejar,"w")
111
+ f.puts @cookie
112
+ f.close
113
+ end
114
+ end
115
+ headers = { 'User-Agent' => UA,
116
+ 'Content-Type' => "multipart/form-data; boundary=#{@boundary}",
117
+ 'Cookie' => @cookie }
118
+ @site = RestClient::Resource.new(@resource, :headers => headers)
119
+ @status = data
120
+ self.untaint
121
+ end
122
+
123
+ # gets the detail for a single ticket/user. If its a ticket, its without
124
+ # history or attachments (to get those use the history method) . If no
125
+ # type is specified, ticket is assumed. takes a single parameter
126
+ # containing the ticket/user id, and returns a hash of RT Fields => values
127
+ #
128
+ # hash = rt.show(822)
129
+ # hash = rt.show("822")
130
+ # hash = rt.show("ticket/822")
131
+ # hash = rt.show(:id => 822)
132
+ # hash = rt.show(:id => "822")
133
+ # hash = rt.show(:id => "ticket/822")
134
+ # hash = rt.show("user/#{login}")
135
+ # email = rt.show("user/somebody")["emailaddress"]
136
+ def show(id)
137
+ id = id[:id] if id.class == Hash
138
+ id = id.to_s
139
+ type = "ticket"
140
+ sid = id
141
+ if id =~ /(\w+)\/(.+)/
142
+ type = $~[1]
143
+ sid = $~[2]
144
+ end
145
+ reply = {}
146
+ resp = @site["#{type}/#{sid}/show"].get
147
+ resp.gsub!(/RT\/\d\.\d\.\d\s\d{3}\s.*\n\n/,"") # toss the HTTP response
148
+ resp.gsub!(/\n\n/,"\n") # remove double spacing, TMail stops at a blank line
149
+ return {:error => resp, } if resp =~ /does not exist./
150
+ th = TMail::Mail.parse(resp)
151
+ th.each_header do |k,v|
152
+ reply["#{k}"] = v.to_s
153
+ end
154
+ reply
155
+ end
156
+
157
+ # Creates a new ticket. Requires a hash that contains RT form fields as
158
+ # the keys. Capitalization is important; use :Queue, not :queue. You
159
+ # will need at least :Queue to create a ticket. For a full list of fields
160
+ # you can use, try "/opt/rt3/bin/rt edit ticket/1". Returns the newly
161
+ # created ticket number, or a complete REST response.
162
+ #
163
+ # id = rt.create( :Queue => "Customer Service",
164
+ # :Cc => "somebody\@email.com",
165
+ # :Subject => "I've fallen and I can't get up",
166
+ # :Text => "I think my hip is broken.\nPlease help.",
167
+ # :"CF.{CustomField}" => "Urgent",
168
+ # :Attachment => "/tmp/broken_hip.jpg" )
169
+ def create(field_hash)
170
+ field_hash[:id] = "ticket/new"
171
+ payload = compose(field_hash)
172
+ resp = @site['ticket/new/edit'].post payload
173
+ new_id = resp.match(/Ticket\s*(\d+)/)
174
+ if new_id.class == MatchData
175
+ new_ticket = new_id[1]
176
+ else
177
+ new_ticket = resp
178
+ end
179
+ new_ticket # return the ticket number, or the full REST response
180
+ end
181
+
182
+ # create a new user. Requires a hash of RT fields => values. Returns
183
+ # the newly created user ID, or the full REST response if there is an error.
184
+ # For a full list of possible parameters that you can specify, look at
185
+ # "/opt/rt/bin/rt edit user/1"
186
+ #
187
+ # new_id = rt.create_user(:Name => "Joe Smith", :EmailAddress => "joes\@here.com")
188
+ def create_user(field_hash)
189
+ field_hash[:id] = "user/new"
190
+ payload = compose(field_hash)
191
+ resp = @site['user/new/edit'].post payload
192
+ new_id = resp.match(/User\s*(\d+)/)
193
+ if new_id.class == MatchData
194
+ new_user = new_id[1]
195
+ else
196
+ new_user = resp
197
+ end
198
+ new_user # return the new user id or the full REST response
199
+ end
200
+
201
+ # edit an existing ticket/user. Requires a hash containing RT
202
+ # form fields as keys. the key :id is required.
203
+ # returns the complete REST response, whatever it is. If the
204
+ # id supplied contains "user/", it edits a user, otherwise
205
+ # it edits a ticket. For a full list of fields you can edit,
206
+ # try "/opt/rt3/bin/rt edit ticket/1"
207
+ #
208
+ # resp = rt.edit(:id => 822, :Status => "resolved")
209
+ # resp = rt.edit(:id => ticket_id, :"CF.{CustomField}" => var)
210
+ # resp = rt.edit(:id => "user/someone", :EMailAddress => "something@here.com")
211
+ # resp = rt.edit(:id => "user/bossman", :Password => "mypass")
212
+ # resp = rt.edit(:id => "user/4306", :Disabled => "1")
213
+ def edit(field_hash)
214
+ if field_hash.has_key? :id
215
+ id = field_hash[:id]
216
+ else
217
+ raise "RT_Client.edit requires a user or ticket id in the 'id' key."
218
+ end
219
+ type = "ticket"
220
+ sid = id
221
+ if id =~ /(\w+)\/(.+)/
222
+ type = $~[1]
223
+ sid = $~[2]
224
+ end
225
+ payload = compose(field_hash)
226
+ resp = @site["#{type}/#{sid}/edit"].post payload
227
+ resp
228
+ end
229
+
230
+ # Comment on a ticket. Requires a hash, which must have an :id key
231
+ # containing the ticket number. Returns the REST response. For a list of
232
+ # fields you can use in a comment, try "/opt/rt3/bin/rt comment ticket/1"
233
+ #
234
+ # rt.comment( :id => id,
235
+ # :Text => "Oh dear, I wonder if the hip smells like almonds?",
236
+ # :Attachment => "/tmp/almonds.gif" )
237
+ def comment(field_hash)
238
+ if field_hash.has_key? :id
239
+ id = field_hash[:id]
240
+ else
241
+ raise "RT_Client.comment requires a Ticket number in the 'id' key."
242
+ end
243
+ field_hash[:Action] = "comment"
244
+ payload = compose(field_hash)
245
+ @site["ticket/#{id}/comment"].post payload
246
+ end
247
+
248
+ # correspond on a ticket. Requires a hash, which must have an :id key
249
+ # containing the ticket number. Returns the REST response. For a list of
250
+ # fields you can use in correspondence, try "/opt/rt3/bin/rt correspond
251
+ # ticket/1"
252
+ #
253
+ # rt.correspond( :id => id,
254
+ # :Text => "We're sending help right away.",
255
+ # :Attachment => "/tmp/admittance.doc" )
256
+ def correspond(field_hash)
257
+ if field_hash.has_key? :id
258
+ if field_hash[:id] =~ /ticket\/(\d+)/
259
+ id = $~[1]
260
+ else
261
+ id = field_hash[:id]
262
+ end
263
+ else
264
+ raise "RT_Client.correspond requires a Ticket number in the 'id' key."
265
+ end
266
+ field_hash[:Action] = "correspond"
267
+ payload = compose(field_hash)
268
+ @site["ticket/#{id}/comment"].post payload
269
+ end
270
+
271
+ # Get a list of tickets matching some criteria.
272
+ # Takes a string Ticket-SQL query and an optional "order by" parameter.
273
+ # The order by is an RT field, prefix it with + for ascending
274
+ # or - for descending.
275
+ # Returns a nested array of arrays containing [ticket number, subject]
276
+ # The outer array is in the order requested.
277
+ #
278
+ # hash = rt.list(:query => "Queue = 'Sales'")
279
+ # hash = rt.list("Queue='Sales'")
280
+ # hash = rt.list(:query => "Queue = 'Sales'", :order => "-Id")
281
+ # hash = rt.list("Queue='Sales'","-Id")
282
+ def list(*params)
283
+ query = params[0]
284
+ order = ""
285
+ if params.size > 1
286
+ order = params[1]
287
+ end
288
+ if params[0].class == Hash
289
+ params = params[0]
290
+ query = params[:query] if params.has_key? :query
291
+ order = params[:order] if params.has_key? :order
292
+ end
293
+ reply = []
294
+ resp = @site["search/ticket/?query=#{URI.escape(query)}&orderby=#{order}&format=s"].get
295
+ raise "Invalid query (#{query})" if resp =~ /Invalid query/
296
+ resp = resp.split("\n") # convert to array of lines
297
+ resp.each do |line|
298
+ f = line.match(/^(\d+):\s*(.*)/)
299
+ reply.push [f[1],f[2]] if f.class == MatchData
300
+ end
301
+ reply
302
+ end
303
+
304
+ # A more extensive(expensive) query then the list method. Takes the same
305
+ # parameters as the list method; a string Ticket-SQL query and optional
306
+ # order, but returns a lot more information. Instead of just the ID and
307
+ # subject, you get back an array of hashes, where each hash represents
308
+ # one ticket, indentical to what you get from the show method (which only
309
+ # acts on one ticket). Use with caution; this can take a long time to
310
+ # execute.
311
+ #
312
+ # array = rt.query("Queue='Sales'")
313
+ # array = rt.query(:query => "Queue='Sales'",:order => "+Id")
314
+ # array = rt.query("Queue='Sales'","+Id")
315
+ # => array[0] = { "id" => "123", "requestors" => "someone@..", etc etc }
316
+ # => array[1] = { "id" => "126", "requestors" => "someone@else..", etc etc }
317
+ # => array[0]["id"] = "123"
318
+ def query(*params)
319
+ query = params[0]
320
+ order = ""
321
+ if params.size > 1
322
+ order = params[1]
323
+ end
324
+ if params[0].class == Hash
325
+ params = params[0]
326
+ query = params[:query] if params.has_key? :query
327
+ order = params[:order] if params.has_key? :order
328
+ end
329
+ replies = []
330
+ resp = @site["search/ticket/?query=#{URI.escape(query)}&orderby=#{order}&format=l"].get
331
+ return replies if resp =~/No matching results./
332
+ raise "Invalid query (#{query})" if resp =~ /Invalid query/
333
+ resp.gsub!(/RT\/\d\.\d\.\d\s\d{3}\s.*\n\n/,"") # strip HTTP response
334
+ tickets = resp.split("\n--\n") # -- occurs between each ticket
335
+ tickets.each do |ticket|
336
+ ticket.gsub!(/^\n/,"") # strip leading blank lines
337
+ ticket.gsub!(/\n\n/,"\n") # remove blank lines for TMail
338
+ th = TMail::Mail.parse(ticket)
339
+ reply = {}
340
+ th.each_header do |k,v|
341
+ reply["#{k}"] = v.to_s
342
+ end
343
+ replies.push reply
344
+ end
345
+ replies
346
+ end
347
+
348
+ # Get a list of history transactions for a ticket. Takes a ticket ID and
349
+ # an optional format parameter. If the format is ommitted, the short
350
+ # format is assumed. If the short format is requested, it returns an
351
+ # array of 2 element arrays, where each 2-element array is [ticket_id,
352
+ # description]. If the long format is requested, it returns an array of
353
+ # hashes, where each hash contains the keys:
354
+ #
355
+ # id:: (history-id)
356
+ # Ticket:: (Ticket this history item belongs to)
357
+ # TimeTaken:: (time entered by the actor that generated this item)
358
+ # Type:: (what type of history item this is)
359
+ # Field:: (what field is affected by this item, if any)
360
+ # OldValue:: (the old value of the Field)
361
+ # NewValue:: (the new value of the Field)
362
+ # Data:: (Additional data about the item)
363
+ # Description:: (Description of this item; same as short format)
364
+ # Content:: (The content of this item)
365
+ # Creator:: (the RT user that created this item)
366
+ # Created:: (Date/time this item was created)
367
+ # Attachments:: (a hash describing attachments to this item)
368
+ #
369
+ # history = rt.history(881)
370
+ # history = rt.history(881,"short")
371
+ # => [["10501"," Ticket created by blah"],["10510"," Comments added by userX"]]
372
+ # history = rt.history(881,"long")
373
+ # history = rt.history(:id => 881, :format => "long")
374
+ # => [{"id" => "6171", "ticket" => "881" ....}, {"id" => "6180", ...} ]
375
+ def history(*params)
376
+ id = params[0]
377
+ format = "short"
378
+ format = params[1].downcase if params.size > 1
379
+ comments = false
380
+ comments = params[2] if params.size > 2
381
+ if params[0].class == Hash
382
+ params = params[0]
383
+ id = params[:id] if params.has_key? :id
384
+ format = params[:format].downcase if params.has_key? :format
385
+ comments = params[:comments] if params.has_key? :comments
386
+ end
387
+ id = id.to_s
388
+ id = $~[1] if id =~ /ticket\/(\d+)/
389
+ resp = @site["ticket/#{id}/history?format=#{format[0,1]}"].get
390
+ if format[0,1] == "s"
391
+ if comments
392
+ h = resp.split("\n").select{ |l| l =~ /^\d+:/ }
393
+ else
394
+ h = resp.split("\n").select{ |l| l =~ /^\d+: [^Comments]/ }
395
+ end
396
+ list = h.map { |l| l.split(":") }
397
+ else
398
+ resp.gsub!(/RT\/\d\.\d\.\d\s\d{3}\s.*\n\n/,"") # toss the HTTP response
399
+ resp.gsub!(/^#.*?\n\n/,"") # toss the 'total" line
400
+ resp.gsub!(/^\n/m,"") # toss blank lines
401
+ items = resp.split("\n--\n")
402
+ list = []
403
+ items.each do |item|
404
+ th = TMail::Mail.parse(item)
405
+ next if not comments and th["type"].to_s =~ /Comment/ # skip comments
406
+ reply = {}
407
+ th.each_header do |k,v|
408
+ attachments = []
409
+ case k
410
+ when "attachments"
411
+ temp = item.match(/Attachments:\s*(.*)/m)
412
+ if temp.class != NilClass
413
+ atarr = temp[1].split("\n")
414
+ atarr.map { |a| a.gsub!(/^\s*/,"") }
415
+ atarr.each do |a|
416
+ i = a.match(/(\d+):\s*(.*)/)
417
+ s={}
418
+ s[:id] = i[1].to_s
419
+ s[:name] = i[2].to_s
420
+ sz = i[2].match(/(.*?)\s*\((.*?)\)/)
421
+ if sz.class == MatchData
422
+ s[:name] = sz[1].to_s
423
+ s[:size] = sz[2].to_s
424
+ end
425
+ attachments.push s
426
+ end
427
+ reply["attachments"] = attachments
428
+ end
429
+ when "content"
430
+ reply["content"] = v.to_s
431
+ temp = item.match(/^Content: (.*?)^\w+:/m) # TMail strips line breaks
432
+ reply["content"] = temp[1] if temp.class != NilClass
433
+ else
434
+ reply["#{k}"] = v.to_s
435
+ end
436
+ end
437
+ list.push reply
438
+ end
439
+ end
440
+ list
441
+ end
442
+
443
+ # Get the detail for a single history item. Needs a ticket ID and a
444
+ # history item ID, returns a hash of RT Fields => values. The hash
445
+ # also contains a special key named "attachments", whose value is
446
+ # an array of hashes, where each hash represents an attachment. The hash
447
+ # keys are :id, :name, and :size.
448
+ #
449
+ # x = rt.history_item(21, 6692)
450
+ # x = rt.history_item(:id => 21, :history => 6692)
451
+ # => x = {"ticket" => "21", "creator" => "somebody", "description" =>
452
+ # => "something happened", "attachments" => [{:name=>"file.txt",
453
+ # => :id=>"3289", size=>"651b"}, {:name=>"another.doc"... }]}
454
+ def history_item(*params)
455
+ id = params[0]
456
+ history = params[1]
457
+ if params[0].class == Hash
458
+ params = params[0]
459
+ id = params[:id] if params.has_key? :id
460
+ history = params[:history] if params.has_key? :history
461
+ end
462
+ reply = {}
463
+ resp = @site["ticket/#{id}/history/id/#{history}"].get
464
+ return reply if resp =~ /not related/ # history id must be related to the ticket id
465
+ resp.gsub!(/RT\/\d\.\d\.\d\s\d{3}\s.*\n\n/,"") # toss the HTTP response
466
+ resp.gsub!(/^#.*?\n\n/,"") # toss the 'total" line
467
+ resp.gsub!(/^\n/m,"") # toss blank lines
468
+ th = TMail::Mail.parse(resp)
469
+ attachments = []
470
+ th.each_header do |k,v|
471
+ case k
472
+ when "attachments"
473
+ temp = resp.match(/Attachments:\s*(.*)[^\w|$]/m)
474
+ if temp.class != NilClass
475
+ atarr = temp[1].split("\n")
476
+ atarr.map { |a| a.gsub!(/^\s*/,"") }
477
+ atarr.each do |a|
478
+ i = a.match(/(\d+):\s*(.*)/)
479
+ s={}
480
+ s[:id] = i[1]
481
+ s[:name] = i[2]
482
+ sz = i[2].match(/(.*?)\s*\((.*?)\)/)
483
+ if sz.class == MatchData
484
+ s[:name] = sz[1]
485
+ s[:size] = sz[2]
486
+ end
487
+ attachments.push s
488
+ end
489
+ reply["#{k}"] = attachments
490
+ end
491
+ when "content"
492
+ reply["content"] = v.to_s
493
+ temp = resp.match(/^Content: (.*?)^\w+:/m) # TMail strips line breaks
494
+ reply["content"] = temp[1] if temp.class != NilClass
495
+ else
496
+ reply["#{k}"] = v.to_s
497
+ end
498
+ end
499
+ reply
500
+ end
501
+
502
+ # Get a list of attachments related to a ticket.
503
+ # Requires a ticket id, returns an array of hashes where each hash
504
+ # represents one attachment. Hash keys are :id, :name, :type, :size.
505
+ # You can optionally request that unnamed attachments be included,
506
+ # the default is to not include them.
507
+ def attachments(*params)
508
+ id = params[0]
509
+ unnamed = params[1]
510
+ if params[0].class == Hash
511
+ params = params[0]
512
+ id = params[:id] if params.has_key? :id
513
+ unnamed = params[:unnamed] if params.has_key? :unnamed
514
+ end
515
+ unnamed = false if unnamed.to_s == "0"
516
+ id = $~[1] if id =~ /ticket\/(\d+)/
517
+ resp = @site["ticket/#{id}/attachments"].get
518
+ resp.gsub!(/RT\/\d\.\d\.\d\s\d{3}\s.*\n\n/,"") # toss the HTTP response
519
+ resp.gsub!(/^\n/m,"") # toss blank lines
520
+ th = TMail::Mail.parse(resp)
521
+ list = th["attachments"].to_s.split(",")
522
+ attachments = []
523
+ list.each do |v|
524
+ attachment = {}
525
+ m=v.match(/(\d+):\s+(.*?)\s+\((.*?)\s+\/\s+(.*?)\)/)
526
+ if m.class == MatchData
527
+ next if m[2] == "(Unnamed)" and !unnamed
528
+ attachment[:id] = m[1]
529
+ attachment[:name] = m[2]
530
+ attachment[:type] = m[3]
531
+ attachment[:size] = m[4]
532
+ attachments.push attachment
533
+ end
534
+ end
535
+ attachments
536
+ end
537
+
538
+ # Get attachment content for single attachment. Requires a ticket ID
539
+ # and an attachment ID, which must be related. If a directory parameter
540
+ # is supplied, the attachment is written to that directory. If not,
541
+ # the attachment content is returned in the hash returned by the
542
+ # function as the key 'content', along with some other keys you always get:
543
+ #
544
+ # transaction:: the transaction id
545
+ # creator:: the user id number who attached it
546
+ # id:: the attachment id
547
+ # filename:: the name of the file
548
+ # contenttype:: MIME content type of the attachment
549
+ # created:: date of the attachment
550
+ # parent:: an attachment id if this was an embedded MIME attachment
551
+ #
552
+ # x = get_attachment(21,3879)
553
+ # x = get_attachment(:ticket => 21, :attachment => 3879)
554
+ # x = get_attachment(:ticket => 21, :attachment => 3879, :dir = "/some/dir")
555
+ def get_attachment(*params)
556
+ tid = params[0]
557
+ aid = params[1]
558
+ dir = nil
559
+ dir = params[2] if params.size > 2
560
+ if params[0].class == Hash
561
+ params = params[0]
562
+ tid = params[:ticket] if params.has_key? :ticket
563
+ aid = params[:attachment] if params.has_key? :attachment
564
+ dir = params[:dir] if params.has_key? :dir
565
+ end
566
+ tid = $~[1] if tid =~ /ticket\/(\d+)/
567
+ resp = @site["ticket/#{tid}/attachments/#{aid}"].get
568
+ resp.gsub!(/RT\/\d\.\d\.\d\s\d{3}\s.*\n\n/,"") # toss HTTP response
569
+ headers = TMail::Mail.parse(resp)
570
+ reply = {}
571
+ headers.each_header do |k,v|
572
+ reply["#{k}"] = v.to_s
573
+ end
574
+ content = resp.match(/Content:\s+(.*)/m)[1]
575
+ content.gsub!(/\n\s{9}/,"\n") # strip leading spaces on each line
576
+ content.chomp!
577
+ content.chomp!
578
+ content.chomp! # 3 carriage returns at the end
579
+ binary = Iconv.conv("ISO-8859-1","UTF-8",content) # convert encoding
580
+ if dir
581
+ fh = File.new("#{dir}/#{headers['Filename'].to_s}","wb")
582
+ fh.write binary
583
+ fh.close
584
+ else
585
+ reply["content"] = binary
586
+ end
587
+ reply
588
+ end
589
+
590
+ # Add a watcher to a ticket, but only if not already a watcher. Takes a
591
+ # ticket ID, an email address (or array of email addresses), and an
592
+ # optional watcher type. If no watcher type is specified, its assumed to
593
+ # be "Cc". Possible watcher types are 'Requestors', 'Cc', and 'AdminCc'.
594
+ #
595
+ # rt.add_watcher(123,"someone@here.com")
596
+ # rt.add_watcher(123,["someone@here.com","another@there.com"])
597
+ # rt.add_watcher(123,"someone@here.com","Requestors")
598
+ # rt.add_watcher(:id => 123, :addr => "someone@here.com")
599
+ # rt.add_watcher(:id => 123, :addr => ["someone@here.com","another@there.com"])
600
+ # rt.add_watcher(:id => 123, :addr => "someone@here.com", :type => "AdminCc")
601
+ def add_watcher(*params)
602
+ tid = params[0]
603
+ addr = []
604
+ type = "cc"
605
+ addr = params[1] if params.size > 1
606
+ type = params[2] if params.size > 2
607
+ if params[0].class == Hash
608
+ params = params[0]
609
+ tid = params[:id] if params.has_key? :id
610
+ addr = params[:addr] if params.has_key? :addr
611
+ type = params[:type] if params.has_key? :type
612
+ end
613
+ addr = addr.to_a.uniq # make it array if its just a string, and remove dups
614
+ type.downcase!
615
+ tobj = show(tid) # get current watchers
616
+ ccs = tobj["cc"].split(", ")
617
+ accs = tobj["admincc"].split(", ")
618
+ reqs = tobj["requestors"].split(", ")
619
+ watchers = ccs | accs | reqs # union of all watchers
620
+ addr.each do |e|
621
+ case type
622
+ when "cc"
623
+ ccs.push(e) if not watchers.include?(e)
624
+ when "admincc"
625
+ accs.push(e) if not watchers.include?(e)
626
+ when "requestors"
627
+ reqs.push(e) if not watchers.include?(e)
628
+ end
629
+ end
630
+ case type
631
+ when "cc"
632
+ edit(:id => tid, :Cc => ccs.join(","))
633
+ when "admincc"
634
+ edit(:id => tid, :AdminCc => accs.join(","))
635
+ when "requestors"
636
+ edit(:id => tid, :Requestors => reqs.join(","))
637
+ end
638
+ end
639
+
640
+ # don't give up the password when the object is inspected
641
+ def inspect # :nodoc:
642
+ mystr = super()
643
+ mystr.gsub!(/(.)pass=.*?([,\}])/,"\\1pass=<hidden>\\2")
644
+ mystr
645
+ end
646
+
647
+ private
648
+
649
+ # Private helper for composing RT's "forms". Requires a hash where the
650
+ # keys are field names for an RT form. If there's a :Text key, the value
651
+ # is modified to insert whitespace on continuation lines. If there's an
652
+ # :Attachment key, the value is assumed to be a comma-separated list of
653
+ # filenames to attach. It returns a multipart MIME body complete
654
+ # with boundaries and headers, suitable for an HTTP POST.
655
+ def compose(fields) # :doc:
656
+ body = ""
657
+ if fields.class != Hash
658
+ raise "RT_Client.compose requires parameters as a hash."
659
+ end
660
+
661
+ # fixup Text field for RFC822 compliance
662
+ if fields.has_key? :Text
663
+ fields[:Text].gsub!(/\n/,"\n ") # insert a space on continuation lines.
664
+ end
665
+
666
+ # attachments
667
+ if fields.has_key? :Attachments
668
+ fields[:Attachment] = fields[:Attachments]
669
+ end
670
+ if fields.has_key? :Attachment
671
+ filenames = fields[:Attachment].split(',')
672
+ i = 0
673
+ filenames.each do |v|
674
+ filename = File.basename(v)
675
+ mime_type = MIME::Types.type_for(v)[0]
676
+ i += 1
677
+ param_name = "attachment_#{i.to_s}"
678
+ body << "--#{@boundary}\r\n"
679
+ body << "Content-Disposition: form-data; "
680
+ body << "name=\"#{URI.escape(param_name.to_s)}\"; "
681
+ body << "filename=\"#{URI.escape(filename)}\"\r\n"
682
+ body << "Content-Type: #{mime_type.simplified}\r\n\r\n"
683
+ body << File.read(v) # oh dear, lets hope you have lots of RAM
684
+ end
685
+ # strip paths from filenames
686
+ fields[:Attachment] = filenames.map {|f| File.basename(f)}.join(',')
687
+ end
688
+
689
+ field_array = fields.map { |k,v| "#{k}: #{v}" }
690
+ content = field_array.join("\n") # our form
691
+ # add the form to the end of any attachments
692
+ body << "--#{@boundary}\r\n"
693
+ body << "Content-Disposition: form-data; "
694
+ body << "name=\"content\";\r\n\r\n"
695
+ body << content << "\r\n"
696
+ body << "--#{@boundary}--\r\n"
697
+ body
698
+ end
699
+ end
data/rt/rtxmlsrv.rb ADDED
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/ruby
2
+
3
+ ## XML RPC service to provide a cross-platform API for
4
+ ## RT ticket creation/maintenance. Essentially just a wrapper
5
+ ## around the rt/client library.
6
+
7
+ require "rubygems" # so we can load gems
8
+ require "rt/client" # rt-client REST library
9
+ require "xmlrpc/server" # that's what we're doing
10
+ require "date" # for parsing arbitrary date formats
11
+
12
+ PORT=8080
13
+ MAX_CONN=50
14
+
15
+ # extend the Hash class to
16
+ # translate string keys into symbol keys
17
+ class Hash # :nodoc:
18
+ def remapkeys!
19
+ n = Hash.new
20
+ self.each_key do |key|
21
+ if key =~ /\{/
22
+ n[eval(":\"#{key}\"")] = self[key]
23
+ else
24
+ n[eval(":#{key}")] = self[key]
25
+ end
26
+ end
27
+ self.replace(n)
28
+ n = nil
29
+ self
30
+ end
31
+ end
32
+
33
+ class TicketSrv
34
+
35
+ def initialize
36
+ end
37
+
38
+ INTERFACE = XMLRPC::interface("rt") {
39
+ meth 'string add_watcher(struct)','Calls RT_Client::add_watcher'
40
+ meth 'array attachments(struct)','Calls RT_Client::attachments'
41
+ meth 'string comment(struct)','Calls RT_Client::comment'
42
+ meth 'string correspond(struct)','Calls RT_Client::correspond'
43
+ meth 'string create(struct)','Calls RT_Client::create'
44
+ meth 'string create_user(struct)','Calls RT_Client::create_user'
45
+ meth 'string edit(struct)','Calls RT_Client::edit'
46
+ meth 'struct get_attachment(struct)','Calls RT_Client::get_attachment'
47
+ meth 'struct history(struct)','Calls RT_Client::history (long form)'
48
+ meth 'struct history_item(struct)','Calls RT_Client::history_item'
49
+ meth 'array list(struct)','Calls RT_Client::list'
50
+ meth 'array query(struct)','Calls RT_Client::query (long form)'
51
+ meth 'struct show(struct)','Calls RT_Client::show'
52
+ }
53
+
54
+ # Allows watchers to be added via RT_Client::add_watcher
55
+ # You need to pass :id, :addr, and optionally :type
56
+ def add_watcher(struct)
57
+ struct.remapkeys!
58
+ if struct.has_key? :user and struct.has_key? :pass
59
+ rt = RT_Client.new(:user => struct[:user], :pass => struct[:pass])
60
+ else
61
+ rt = RT_Client.new
62
+ end
63
+ val = rt.add_watcher(struct)
64
+ rt = nil
65
+ val
66
+ end
67
+
68
+ # Gets a list of attachments via RT_Client::attachments
69
+ # You need to pass :id, and optionally :unnamed
70
+ def attachments(struct)
71
+ struct.remapkeys!
72
+ if struct.has_key? :user and struct.has_key? :pass
73
+ rt = RT_Client.new(:user => struct[:user], :pass => struct[:pass])
74
+ else
75
+ rt = RT_Client.new
76
+ end
77
+ rt = RT_Client.new
78
+ val = rt.attachments(struct)
79
+ rt = nil
80
+ val
81
+ end
82
+
83
+ # Adds comments to tickets via RT_Client::comment
84
+ def comment(struct)
85
+ struct.remapkeys!
86
+ if struct.has_key? :user and struct.has_key? :pass
87
+ rt = RT_Client.new(:user => struct[:user], :pass => struct[:pass])
88
+ else
89
+ rt = RT_Client.new
90
+ end
91
+ val = rt.comment(struct)
92
+ rt = nil
93
+ val
94
+ end
95
+
96
+ # Allows new tickets to be created via RT_Client::correspond
97
+ def correspond(struct)
98
+ struct.remapkeys!
99
+ if struct.has_key? :user and struct.has_key? :pass
100
+ rt = RT_Client.new(:user => struct[:user], :pass => struct[:pass])
101
+ else
102
+ rt = RT_Client.new
103
+ end
104
+ val = rt.correspond(struct)
105
+ rt = nil
106
+ val
107
+ end
108
+
109
+ # Allows new tickets to be created via RT_Client::create
110
+ def create(struct)
111
+ struct.remapkeys!
112
+ if struct.has_key? :user and struct.has_key? :pass
113
+ rt = RT_Client.new(:user => struct[:user], :pass => struct[:pass])
114
+ else
115
+ rt = RT_Client.new
116
+ end
117
+ val = rt.create(struct)
118
+ rt = nil
119
+ val
120
+ end
121
+
122
+ # Allows new users to be created via RT_Client::create_user
123
+ def create_user(struct)
124
+ struct.remapkeys!
125
+ if struct.has_key? :user and struct.has_key? :pass
126
+ rt = RT_Client.new(:user => struct[:user], :pass => struct[:pass])
127
+ else
128
+ rt = RT_Client.new
129
+ end
130
+ val = rt.create_user(struct)
131
+ rt = nil
132
+ val
133
+ end
134
+
135
+ # Allows existing ticket to be modified via RT_Client::edit
136
+ def edit(struct)
137
+ struct.remapkeys!
138
+ if struct.has_key? :user and struct.has_key? :pass
139
+ rt = RT_Client.new(:user => struct[:user], :pass => struct[:pass])
140
+ else
141
+ rt = RT_Client.new
142
+ end
143
+ val = rt.edit(struct)
144
+ rt = nil
145
+ val
146
+ end
147
+
148
+ # Retrieves attachments via RT_Client::get_attachment
149
+ def get_attachment(struct)
150
+ struct.remapkeys!
151
+ if struct.has_key? :user and struct.has_key? :pass
152
+ rt = RT_Client.new(:user => struct[:user], :pass => struct[:pass])
153
+ else
154
+ rt = RT_Client.new
155
+ end
156
+ val = rt.get_attachment(struct)
157
+ rt = nil
158
+ val
159
+ end
160
+
161
+ # Gets the history of a ticket via RT_Client::history
162
+ def history(struct)
163
+ struct.remapkeys!
164
+ if struct.has_key? :user and struct.has_key? :pass
165
+ rt = RT_Client.new(:user => struct[:user], :pass => struct[:pass])
166
+ else
167
+ rt = RT_Client.new
168
+ end
169
+ val = rt.history(struct)
170
+ rt = nil
171
+ val
172
+ end
173
+
174
+ # Gets a single history item via RT_Client::history_item
175
+ def history_item(struct)
176
+ struct.remapkeys!
177
+ if struct.has_key? :user and struct.has_key? :pass
178
+ rt = RT_Client.new(:user => struct[:user], :pass => struct[:pass])
179
+ else
180
+ rt = RT_Client.new
181
+ end
182
+ val = rt.history_item(struct)
183
+ rt = nil
184
+ val
185
+ end
186
+
187
+ # Gets a list of tickets via RT_Client::list
188
+ def list(struct)
189
+ struct.remapkeys!
190
+ if struct.has_key? :user and struct.has_key? :pass
191
+ rt = RT_Client.new(:user => struct[:user], :pass => struct[:pass])
192
+ else
193
+ rt = RT_Client.new
194
+ end
195
+ val = rt.list(struct)
196
+ rt = nil
197
+ val
198
+ end
199
+
200
+ # Gets a list of tickets via RT_Client::query
201
+ def query(struct)
202
+ struct.remapkeys!
203
+ if struct.has_key? :user and struct.has_key? :pass
204
+ rt = RT_Client.new(:user => struct[:user], :pass => struct[:pass])
205
+ else
206
+ rt = RT_Client.new
207
+ end
208
+ val = rt.query(struct)
209
+ rt = nil
210
+ val
211
+ end
212
+
213
+ # Gets detail (minus history/attachments) via RT_Client::show
214
+ def show(struct)
215
+ struct.remapkeys!
216
+ if struct.has_key? :user and struct.has_key? :pass
217
+ rt = RT_Client.new(:user => struct[:user], :pass => struct[:pass])
218
+ else
219
+ rt = RT_Client.new
220
+ end
221
+ val = rt.show(struct)
222
+ rt = nil
223
+ val
224
+ end
225
+
226
+ end # class TicketSrv
227
+
228
+ pid = fork do
229
+ Signal.trap('HUP','IGNORE')
230
+ # set up a log file
231
+ logfile = File.dirname(__FILE__) + "/ticketsrv.log"
232
+ accfile = File.dirname(__FILE__) + "/access.log"
233
+ acc = File.open(accfile,"a+")
234
+ $stderr.reopen acc # redirect $stderr to the log as well
235
+ # determine the IP address to listen on and create the server
236
+ sock = Socket.getaddrinfo(Socket.gethostname,PORT,Socket::AF_INET,Socket::SOCK_STREAM)
237
+ $s = XMLRPC::Server.new(sock[0][1], sock[0][3], MAX_CONN, logfile)
238
+ $s.set_parser(XMLRPC::XMLParser::XMLStreamParser.new)
239
+ $s.add_handler(TicketSrv::INTERFACE, TicketSrv.new)
240
+ $s.add_introspection
241
+ $s.serve # start serving
242
+ $stderr.reopen STDERR
243
+ acc.close
244
+ end
245
+ Process.detach(pid)
246
+
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rt-client
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.2"
5
+ platform: ruby
6
+ authors:
7
+ - Tom Lahti
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-27 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rest-client
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0.9"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: tmail
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.2.0
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: mime-types
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "1.16"
44
+ version:
45
+ description: RT_Client is a ruby object that accesses the REST interface version 1.0 of a Request Tracker instance. See http://www.bestpractical.com/ for Request Tracker.
46
+ email: toml@bitstatement.net
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ extra_rdoc_files: []
52
+
53
+ files:
54
+ - rt/client.rb
55
+ - rt/rtxmlsrv.rb
56
+ has_rdoc: true
57
+ homepage:
58
+ post_install_message:
59
+ rdoc_options:
60
+ - --inline-source
61
+ - --main
62
+ - RT_Client
63
+ require_paths:
64
+ - .
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 1.8.6
70
+ version:
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ version:
77
+ requirements:
78
+ - A working installation of RT with the REST 1.0 interface
79
+ rubyforge_project:
80
+ rubygems_version: 1.2.0
81
+ signing_key:
82
+ specification_version: 2
83
+ summary: Ruby object for RT access via REST
84
+ test_files: []
85
+