Changeset 4492

Show
Ignore:
Timestamp:
10/13/08 18:09:46 (1 month ago)
Author:
coling
Message:

Add a zendesk upload (it's actually quite generic REST but it doesn't actually work right now... sooooooo)

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • clientsplugin/0.11/clients/action_zendesk_forum.py

    • Property svn:mergeinfo set
    r4452 r4492  
    55import time 
    66import codecs 
     7import httplib 
     8import urlparse 
    79from datetime import datetime 
    810from StringIO import StringIO 
     
    1820 
    1921 
    20 class ClientActionEmail(Component): 
     22class ClientActionZendesk(Component): 
    2123  implements(IClientActionProvider) 
    2224 
     
    2527 
    2628  def get_name(self): 
    27     return "Send Email
     29    return "Post to Zendesk
    2830 
    2931  def get_description(self): 
    30     return "Send an email to a certain list of addresses
     32    return "Post the summary to a Zendesk forum topic
    3133 
    3234  def options(self, client=None): 
    3335    if client is None: 
    34       yield {'name': 'XSLT', 'description': 'Formatting XSLT to convert the summary to an email'} 
     36      yield {'name': 'XSLT', 'description': 'Formatting XSLT to convert the summary to a Zendesk compatible post', 'type': 'large'} 
     37      yield {'name': 'Username', 'description': 'Zendesk username', 'type': 'medium'} 
     38      yield {'name': 'Password', 'description': 'Zendesk password', 'type': 'medium'} 
     39      yield {'name': 'Method', 'description': 'Interaction Method', 'type': 'list', 'vals': ['POST', 'PUT']} 
    3540    else: 
    36       yield {'name': 'Email Addresses', 'description': 'Comma separated list of email addresses'} 
     41      yield {'name': 'Zendesk URI', 'description': 'Zendesk Forum REST URI', 'type': 'medium'} 
    3742 
    3843 
     
    4752      return False 
    4853 
    49     if not event.action_client_options.has_key('Email Addresses') or not event.action_client_options['Email Addresses']['value']: 
    50       return False 
    51  
    52     self.emails = [] 
    53     for email in event.action_client_options['Email Addresses']['value'].replace(',', ' ').split(' '): 
    54       if '' != email.strip(): 
    55         self.emails.append(email.strip()) 
    56  
    57     if not self.emails: 
    58       return False 
     54    if not event.action_options.has_key('Username') or not event.action_options['Username']['value']: 
     55      return False 
     56    self.username = event.action_options['Username']['value'] 
     57 
     58    if not event.action_options.has_key('Password') or not event.action_options['Password']['value']: 
     59      return False 
     60    self.password = event.action_options['Password']['value'] 
     61 
     62    if not event.action_options.has_key('Method') or not event.action_options['Method']['value']: 
     63      return False 
     64    self.method = event.action_options['Method']['value'] 
     65 
     66    if not event.action_client_options.has_key('Zendesk URI') or not event.action_client_options['Zendesk URI']['value']: 
     67      return False 
     68    self.uri = event.action_client_options['Zendesk URI']['value'] 
    5969 
    6070    return True 
     
    6272 
    6373  def perform(self, req, summary): 
     74    def parseuri(uri):  
     75      """Parse URI, return (host, port, path) tuple. 
     76 
     77      >>> parseuri('http://example.org/testing?somequery#frag') 
     78      ('example.org', 80, '/testing?somequery') 
     79      >>> parseuri('http://example.net:8080/test.html') 
     80      ('example.net', 8080, '/test.html') 
     81      """ 
     82 
     83      scheme, netplace, path, query, fragid = urlparse.urlsplit(uri) 
     84 
     85      if ':' in netplace:  
     86        host, port = netplace.split(':', 2) 
     87        port = int(port) 
     88      else: host, port = netplace, 80 
     89 
     90      if query: path += '?' + query 
     91 
     92      return host, port, path 
     93 
     94 
     95 
    6496    if summary is None: 
    6597      return False 
    66     self.config = self.env.config 
    67     self.encoding = 'utf-8' 
    68     subject = 'Ticket Summary for %s' % self.client 
    69  
    70     if not self.config.getbool('notification', 'smtp_enabled'): 
    71       return False 
    72     smtp_server = self.config['notification'].get('smtp_server') 
    73     smtp_port = self.config['notification'].getint('smtp_port') 
    74     from_email = self.config['notification'].get('smtp_from') 
    75     from_name = self.config['notification'].get('smtp_from_name') 
    76     replyto_email = self.config['notification'].get('smtp_replyto') 
    77     from_email = from_email or replyto_email 
    78     if not from_email: 
    79       return False 
    80      
    81     # Authentication info (optional) 
    82     user_name = self.config['notification'].get('smtp_user') 
    83     password = self.config['notification'].get('smtp_password') 
    84      
    85     # Thanks to the author of this recipe: 
    86     # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/473810 
    87      
    88     from email.MIMEMultipart import MIMEMultipart 
    89     from email.MIMEText import MIMEText 
    90     from email.MIMEImage import MIMEImage 
    91     from email.Charset import add_charset, SHORTEST 
    92     add_charset( 'utf-8', SHORTEST, None, None ) 
    93  
    94     projname = self.config.get('project', 'name') 
    95      
    96     # Create the root message and fill in the from, to, and subject headers 
    97     msg_root = MIMEMultipart('alternative') 
    98     msg_root['To'] = str(', ').join(self.emails) 
    99      
    100     msg_root['X-Mailer'] = 'ClientsPlugin for Trac' 
    101     #msg_root['X-Trac-Version'] =  __version__ 
    102     msg_root['X-Trac-Project'] =  projname 
    103     msg_root['Precedence'] = 'bulk' 
    104     msg_root['Auto-Submitted'] = 'auto-generated' 
    105     msg_root['Subject'] = subject 
    106     msg_root['From'] = '%s <%s>' % (from_name or projname, from_email) 
    107     msg_root['Reply-To'] = replyto_email 
    108     msg_root.preamble = 'This is a multi-part message in MIME format.' 
    109      
    110     view = 'plain' 
    111     arg = "'%s'" % view 
    112     result = self.transform(summary, view=arg) 
    113     msg_text = MIMEText(str(result), view, self.encoding) 
    114     msg_root.attach(msg_text) 
    115      
    116     msg_related = MIMEMultipart('related') 
    117     msg_root.attach(msg_related) 
    118      
    119     view = 'html' 
    120     arg = "'%s'" % view 
    121     result = self.transform(summary, view=arg) 
    122     #file = open('/tmp/send-client-email.html', 'w') 
    123     #file.write(str(result)) 
    124     #file.close() 
    125  
    126     msg_text = MIMEText(str(result), view, self.encoding) 
    127     msg_related.attach(msg_text) 
    128      
    129     # Handle image embedding... 
    130     view = 'images' 
    131     arg = "'%s'" % view 
    132     result = self.transform(summary, view=arg) 
    133     if result: 
    134       images = result.getroot() 
    135       if images: 
    136         for img in images: 
    137           if 'img' != img.tag: 
    138             continue 
    139           if not img.get('id') or not img.get('src'): 
    140             continue 
    141            
    142           fp = open(img.get('src'), 'rb') 
    143           if not fp: 
    144             continue 
    145           msg_img = MIMEImage(fp.read()) 
    146           fp.close() 
    147           msg_img.add_header('Content-ID', '<%s>' % img.get('id')) 
    148           msg_related.attach(msg_img) 
    149      
    150     # Send the email 
    151     import smtplib 
    152     smtp = smtplib.SMTP() #smtp_server, smtp_port) 
    153     if False and user_name: 
    154         smtp.login(user_name, password) 
    155     smtp.connect() 
    156     smtp.sendmail(from_email, self.emails, msg_root.as_string()) 
    157     smtp.quit() 
     98 
     99    result = self.transform(summary) 
     100 
     101    username = self.username 
     102    password = self.password 
     103    uri = self.uri 
     104 
     105    host, port, path = parseuri(uri) 
     106 
     107    redirect = set([301, 302, 307]) 
     108    authenticate = set([401]) 
     109    okay = set([200, 201, 204]) 
     110 
     111    authorized = False 
     112    authorization = None 
     113    tries = 0 
     114    verbose = True 
     115 
     116    while True:  
     117      # Attempt to HTTP PUT the data 
     118      h = httplib.HTTPConnection(host, port) 
     119 
     120      h.putrequest('PUT', path) 
     121 
     122      h.putheader('User-Agent', 'Trac/1.0') 
     123      h.putheader('Connection', 'keep-alive') 
     124      h.putheader('Transfer-Encoding', 'chunked') 
     125      h.putheader('Expect', '100-continue') 
     126      h.putheader('Accept', 'application/xml') 
     127      h.putheader('Content-Type', 'text/xml') 
     128      h.putheader('Content-Length', len(str(result))) 
     129      if authorization:  
     130         h.putheader('Authorization', authorization) 
     131      h.endheaders() 
     132 
     133      # Chunked transfer encoding 
     134      # Cf. 'All HTTP/1.1 applications MUST be able to receive and  
     135      # decode the "chunked" transfer-coding' 
     136      # - http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html 
     137      while True:  
     138         #bytes = f.read(2048) 
     139         bytes = str(result) 
     140         if not bytes: break 
     141 
     142         length = len(bytes) 
     143         h.send('%X\r\n' % length) 
     144         h.send(bytes + '\r\n') 
     145         break 
     146      h.send('0\r\n\r\n') 
     147 
     148      resp = h.getresponse() 
     149      status = resp.status # an int 
     150 
     151      # Got a response, now decide how to act upon it 
     152      if status in redirect:  
     153         location = resp.getheader('Location') 
     154         uri = urlparse.urljoin(uri, location) 
     155         host, port, path = parseuri(uri) 
     156 
     157         # We may have to authenticate again 
     158         if authorization:  
     159            authorization = None 
     160 
     161      elif status in authenticate:  
     162         # If we've done this already, break 
     163         if authorization:  
     164            # barf("Going around in authentication circles") 
     165            print "Authentication failed" 
     166            return False 
     167 
     168         if not (username and password):  
     169            print "Need a username and password to authenticate with" 
     170            return False 
     171 
     172         # Get the scheme: Basic or Digest? 
     173         wwwauth = resp.msg['www-authenticate'] # We may need this again 
     174         wauth = wwwauth.lstrip(' \t') # Hence use wauth not wwwauth here 
     175         wauth = wwwauth.replace('\t', ' ') 
     176         i = wauth.index(' ') 
     177         scheme = wauth[:i].lower() 
     178 
     179         if scheme in set(['basic', 'digest']):  
     180            if verbose:  
     181               msg = "Performing %s Authentication..." % scheme.capitalize() 
     182               print >> sys.stderr, msg 
     183         else: 
     184            print "Unknown authentication scheme: %s" % scheme 
     185            return False 
     186 
     187         if scheme == 'basic':  
     188            import base64 
     189            userpass = username + ':' + password 
     190            userpass = base64.encodestring(userpass).strip() 
     191            authorized, authorization = True, 'Basic ' + userpass 
     192 
     193         elif scheme == 'digest':  
     194            if verbose:  
     195               msg = "uses fragile, undocumented features in urllib2" 
     196               print >> sys.stderr, "Warning! Digest Auth %s" % msg 
     197 
     198            import urllib2 # See warning above 
     199 
     200            passwd = type('Password', (object,), { 
     201               'find_user_password': lambda self, *args: (username, password),  
     202               'add_password': lambda self, *args: None 
     203            })() 
     204 
     205            xreq = type('Request', (object,), {  
     206               'get_full_url': lambda self: uri,  
     207               'has_data': lambda self: None,  
     208               'get_method': lambda self: 'PUT',  
     209               'get_selector': lambda self: path 
     210            })() 
     211 
     212            # Cf. urllib2.AbstractDigestAuthHandler.retry_http_digest_auth 
     213            auth = urllib2.AbstractDigestAuthHandler(passwd) 
     214            token, challenge = wwwauth.split(' ', 1) 
     215            chal = urllib2.parse_keqv_list(urllib2.parse_http_list(challenge)) 
     216            userpass = auth.get_authorization(xreq, chal) 
     217            authorized, authorization = True, 'Digest ' + userpass 
     218 
     219      elif status in okay:  
     220         if (username and password) and (not authorized):  
     221            msg = "Warning! The supplied username and password went unused" 
     222            print >> sys.stderr, msg 
     223 
     224         if verbose:  
     225            resultLine = "Success! Resource %s" 
     226            statuses = {200: 'modified', 201: 'created', 204: 'modified'} 
     227            print resultLine % statuses[status] 
     228 
     229            statusLine = "Response-Status: %s %s" 
     230            print statusLine % (status, resp.reason) 
     231 
     232            body = resp.read(58) 
     233            body = body.rstrip('\r\n') 
     234            body = body.encode('string_escape') 
     235 
     236            if len(body) >= 58:  
     237               body = body[:57] + '[...]' 
     238 
     239            bodyLine = 'Response-Body: "%s"' 
     240            print bodyLine % body 
     241         break 
     242 
     243      # @@ raise PutError, do the catching in main? 
     244      else:  
     245        print 'Got "%s %s"' % (status, resp.reason) 
     246        return False 
     247 
     248      tries += 1 
     249      if tries >= 50:  
     250         print "Too many redirects" 
     251         return False 
     252 
     253    print str(result) 
    158254    return True 
  • clientsplugin/0.11/setup.py

    r4441 r4492  
    3232            'clients.action = clients.action', 
    3333            'clients.action_email = clients.action_email', 
     34            'clients.action_zendesk_forum = clients.action_zendesk_forum', 
    3435        ] 
    3536    })