rt-client 0.3.3 → 0.3.4

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