rt-client 0.2

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