| Index: my_activity.py
 | 
| diff --git a/my_activity.py b/my_activity.py
 | 
| index 8468772cf978f41bdc275cdbf935e781b260e203..e7af09e3d670deafef4756a827f715e0c5585903 100755
 | 
| --- a/my_activity.py
 | 
| +++ b/my_activity.py
 | 
| @@ -42,6 +42,9 @@ import gerrit_util
 | 
|  import rietveld
 | 
|  from third_party import upload
 | 
|  
 | 
| +import auth
 | 
| +from third_party import httplib2
 | 
| +
 | 
|  try:
 | 
|    from dateutil.relativedelta import relativedelta # pylint: disable=F0401
 | 
|  except ImportError:
 | 
| @@ -99,10 +102,6 @@ gerrit_instances = [
 | 
|      'url': 'chrome-internal-review.googlesource.com',
 | 
|      'shorturl': 'crosreview.com/i',
 | 
|    },
 | 
| -  {
 | 
| -    'host': 'gerrit.chromium.org',
 | 
| -    'port': 29418,
 | 
| -  },
 | 
|  ]
 | 
|  
 | 
|  google_code_projects = [
 | 
| @@ -132,36 +131,6 @@ google_code_projects = [
 | 
|    },
 | 
|  ]
 | 
|  
 | 
| -# Uses ClientLogin to authenticate the user for Google Code issue trackers.
 | 
| -def get_auth_token(email):
 | 
| -  # KeyringCreds will use the system keyring on the first try, and prompt for
 | 
| -  # a password on the next ones.
 | 
| -  creds = upload.KeyringCreds('code.google.com', 'code.google.com', email)
 | 
| -  for _ in xrange(3):
 | 
| -    email, password = creds.GetUserCredentials()
 | 
| -    url = 'https://www.google.com/accounts/ClientLogin'
 | 
| -    data = urllib.urlencode({
 | 
| -        'Email': email,
 | 
| -        'Passwd': password,
 | 
| -        'service': 'code',
 | 
| -        'source': 'chrome-my-activity',
 | 
| -        'accountType': 'GOOGLE',
 | 
| -    })
 | 
| -    req = urllib2.Request(url, data=data, headers={'Accept': 'text/plain'})
 | 
| -    try:
 | 
| -      response = urllib2.urlopen(req)
 | 
| -      response_body = response.read()
 | 
| -      response_dict = dict(x.split('=')
 | 
| -                           for x in response_body.split('\n') if x)
 | 
| -      return response_dict['Auth']
 | 
| -    except urllib2.HTTPError, e:
 | 
| -      print e
 | 
| -
 | 
| -  print 'Unable to authenticate to code.google.com.'
 | 
| -  print 'Some issues may be missing.'
 | 
| -  return None
 | 
| -
 | 
| -
 | 
|  def username(email):
 | 
|    """Keeps the username of an email address."""
 | 
|    return email and email.split('@', 1)[0]
 | 
| @@ -230,31 +199,12 @@ class MyActivity(object):
 | 
|    # Check the codereview cookie jar to determine which Rietveld instances to
 | 
|    # authenticate to.
 | 
|    def check_cookies(self):
 | 
| -    cookie_file = os.path.expanduser('~/.codereview_upload_cookies')
 | 
| -    if not os.path.exists(cookie_file):
 | 
| -      print 'No Rietveld cookie file found.'
 | 
| -      cookie_jar = []
 | 
| -    else:
 | 
| -      cookie_jar = cookielib.MozillaCookieJar(cookie_file)
 | 
| -      try:
 | 
| -        cookie_jar.load()
 | 
| -        print 'Found cookie file: %s' % cookie_file
 | 
| -      except (cookielib.LoadError, IOError):
 | 
| -        print 'Error loading Rietveld cookie file: %s' % cookie_file
 | 
| -        cookie_jar = []
 | 
| -
 | 
|      filtered_instances = []
 | 
|  
 | 
|      def has_cookie(instance):
 | 
| -      for cookie in cookie_jar:
 | 
| -        if cookie.name == 'SACSID' and cookie.domain == instance['url']:
 | 
| -          return True
 | 
| -      if self.options.auth:
 | 
| -        return get_yes_or_no('No cookie found for %s. Authorize for this '
 | 
| -                             'instance? (may require application-specific '
 | 
| -                             'password)' % instance['url'])
 | 
| -      filtered_instances.append(instance)
 | 
| -      return False
 | 
| +      auth_config = auth.extract_auth_config_from_options(self.options)
 | 
| +      a = auth.get_authenticator_for_host(instance['url'], auth_config)
 | 
| +      return a.has_cached_credentials()
 | 
|  
 | 
|      for instance in rietveld_instances:
 | 
|        instance['auth'] = has_cookie(instance)
 | 
| @@ -461,107 +411,51 @@ class MyActivity(object):
 | 
|        })
 | 
|      return ret
 | 
|  
 | 
| -  def google_code_issue_search(self, instance):
 | 
| -    time_format = '%Y-%m-%dT%T'
 | 
| -    # See http://code.google.com/p/support/wiki/IssueTrackerAPI
 | 
| -    # q=<owner>@chromium.org does a full text search for <owner>@chromium.org.
 | 
| -    # This will accept the issue if owner is the owner or in the cc list. Might
 | 
| -    # have some false positives, though.
 | 
| -
 | 
| -    # Don't filter normally on modified_before because it can filter out things
 | 
| -    # that were modified in the time period and then modified again after it.
 | 
| -    gcode_url = ('https://code.google.com/feeds/issues/p/%s/issues/full' %
 | 
| -                 instance['name'])
 | 
| -
 | 
| -    gcode_data = urllib.urlencode({
 | 
| -        'alt': 'json',
 | 
| -        'max-results': '100000',
 | 
| -        'q': '%s' % self.user,
 | 
| -        'published-max': self.modified_before.strftime(time_format),
 | 
| -        'updated-min': self.modified_after.strftime(time_format),
 | 
| -    })
 | 
| -
 | 
| -    opener = urllib2.build_opener()
 | 
| -    if self.google_code_auth_token:
 | 
| -      opener.addheaders = [('Authorization', 'GoogleLogin auth=%s' %
 | 
| -                            self.google_code_auth_token)]
 | 
| -    gcode_json = None
 | 
| -    try:
 | 
| -      gcode_get = opener.open(gcode_url + '?' + gcode_data)
 | 
| -      gcode_json = json.load(gcode_get)
 | 
| -      gcode_get.close()
 | 
| -    except urllib2.HTTPError, _:
 | 
| -      print 'Unable to access ' + instance['name'] + ' issue tracker.'
 | 
| -
 | 
| -    if not gcode_json or 'entry' not in gcode_json['feed']:
 | 
| -      return []
 | 
| -
 | 
| -    issues = gcode_json['feed']['entry']
 | 
| -    issues = map(partial(self.process_google_code_issue, instance), issues)
 | 
| -    issues = filter(self.filter_issue, issues)
 | 
| -    issues = sorted(issues, key=lambda i: i['modified'], reverse=True)
 | 
| -    return issues
 | 
| -
 | 
| -  def process_google_code_issue(self, project, issue):
 | 
| -    ret = {}
 | 
| -    ret['created'] = datetime_from_google_code(issue['published']['$t'])
 | 
| -    ret['modified'] = datetime_from_google_code(issue['updated']['$t'])
 | 
| -
 | 
| -    ret['owner'] = ''
 | 
| -    if 'issues$owner' in issue:
 | 
| -      ret['owner'] = issue['issues$owner']['issues$username']['$t']
 | 
| -    ret['author'] = issue['author'][0]['name']['$t']
 | 
| -
 | 
| -    if 'shorturl' in project:
 | 
| -      issue_id = issue['id']['$t']
 | 
| -      issue_id = issue_id[issue_id.rfind('/') + 1:]
 | 
| -      ret['url'] = 'http://%s/%d' % (project['shorturl'], int(issue_id))
 | 
| -    else:
 | 
| -      issue_url = issue['link'][1]
 | 
| -      if issue_url['rel'] != 'alternate':
 | 
| -        raise RuntimeError
 | 
| -      ret['url'] = issue_url['href']
 | 
| -    ret['header'] = issue['title']['$t']
 | 
| -
 | 
| -    ret['replies'] = self.get_google_code_issue_replies(issue)
 | 
| -    return ret
 | 
| -
 | 
| -  def get_google_code_issue_replies(self, issue):
 | 
| -    """Get all the comments on the issue."""
 | 
| -    replies_url = issue['link'][0]
 | 
| -    if replies_url['rel'] != 'replies':
 | 
| -      raise RuntimeError
 | 
| -
 | 
| -    replies_data = urllib.urlencode({
 | 
| -        'alt': 'json',
 | 
| -        'fields': 'entry(published,author,content)',
 | 
| +  def project_hosting_issue_search(self, instance):
 | 
| +    auth_config = auth.extract_auth_config_from_options(self.options)
 | 
| +    authenticator = auth.get_authenticator_for_host(
 | 
| +        "code.google.com", auth_config)
 | 
| +    http = authenticator.authorize(httplib2.Http())
 | 
| +    url = "https://www.googleapis.com/projecthosting/v2/projects/%s/issues" % (
 | 
| +       instance["name"])
 | 
| +    epoch = datetime.utcfromtimestamp(0)
 | 
| +    user_str = '%s@chromium.org' % self.user
 | 
| +
 | 
| +    query_data = urllib.urlencode({
 | 
| +      'maxResults': 10000,
 | 
| +      'q': user_str,
 | 
| +      'publishedMax': '%d' % (self.modified_before - epoch).total_seconds(),
 | 
| +      'updatedMin': '%d' % (self.modified_after - epoch).total_seconds(),
 | 
|      })
 | 
| -
 | 
| -    opener = urllib2.build_opener()
 | 
| -    opener.addheaders = [('Authorization', 'GoogleLogin auth=%s' %
 | 
| -                          self.google_code_auth_token)]
 | 
| -    try:
 | 
| -      replies_get = opener.open(replies_url['href'] + '?' + replies_data)
 | 
| -    except urllib2.HTTPError, _:
 | 
| +    url = url + '?' + query_data
 | 
| +    _, body = http.request(url)
 | 
| +    content = json.loads(body)
 | 
| +    if not content:
 | 
| +      print "Unable to parse %s response from projecthosting." % (
 | 
| +          instance["name"])
 | 
|        return []
 | 
|  
 | 
| -    replies_json = json.load(replies_get)
 | 
| -    replies_get.close()
 | 
| -    return self.process_google_code_issue_replies(replies_json)
 | 
| +    issues = []
 | 
| +    if 'items' in content:
 | 
| +      items = content['items']
 | 
| +      for item in items:
 | 
| +        issue = {
 | 
| +          "header": item["title"],
 | 
| +          "created": item["published"],
 | 
| +          "modified": item["updated"],
 | 
| +          "author": item["author"]["name"],
 | 
| +          "url": "https://code.google.com/p/%s/issues/detail?id=%s" % (
 | 
| +              instance["name"], item["id"]),
 | 
| +          "comments": []
 | 
| +        }
 | 
| +        if 'owner' in item:
 | 
| +          issue['owner'] = item['owner']['name']
 | 
| +        else:
 | 
| +          issue['owner'] = 'None'
 | 
| +        if issue['owner'] == user_str or issue['author'] == user_str:
 | 
| +          issues.append(issue)
 | 
|  
 | 
| -  @staticmethod
 | 
| -  def process_google_code_issue_replies(replies):
 | 
| -    if 'entry' not in replies['feed']:
 | 
| -      return []
 | 
| -
 | 
| -    ret = []
 | 
| -    for entry in replies['feed']['entry']:
 | 
| -      e = {}
 | 
| -      e['created'] = datetime_from_google_code(entry['published']['$t'])
 | 
| -      e['content'] = entry['content']['$t']
 | 
| -      e['author'] = entry['author'][0]['name']['$t']
 | 
| -      ret.append(e)
 | 
| -    return ret
 | 
| +    return issues
 | 
|  
 | 
|    def print_heading(self, heading):
 | 
|      print
 | 
| @@ -648,10 +542,6 @@ class MyActivity(object):
 | 
|      # required.
 | 
|      pass
 | 
|  
 | 
| -  def auth_for_issues(self):
 | 
| -    self.google_code_auth_token = (
 | 
| -        get_auth_token(self.options.local_user + '@chromium.org'))
 | 
| -
 | 
|    def get_changes(self):
 | 
|      for instance in rietveld_instances:
 | 
|        self.changes += self.rietveld_search(instance, owner=self.user)
 | 
| @@ -682,7 +572,7 @@ class MyActivity(object):
 | 
|  
 | 
|    def get_issues(self):
 | 
|      for project in google_code_projects:
 | 
| -      self.issues += self.google_code_issue_search(project)
 | 
| +      self.issues += self.project_hosting_issue_search(project)
 | 
|  
 | 
|    def print_issues(self):
 | 
|      if self.issues:
 | 
| @@ -841,17 +731,18 @@ def main():
 | 
|      my_activity.auth_for_changes()
 | 
|    if options.reviews:
 | 
|      my_activity.auth_for_reviews()
 | 
| -  if options.issues:
 | 
| -    my_activity.auth_for_issues()
 | 
|  
 | 
|    print 'Looking up activity.....'
 | 
|  
 | 
| -  if options.changes:
 | 
| -    my_activity.get_changes()
 | 
| -  if options.reviews:
 | 
| -    my_activity.get_reviews()
 | 
| -  if options.issues:
 | 
| -    my_activity.get_issues()
 | 
| +  try:
 | 
| +    if options.changes:
 | 
| +      my_activity.get_changes()
 | 
| +    if options.reviews:
 | 
| +      my_activity.get_reviews()
 | 
| +    if options.issues:
 | 
| +      my_activity.get_issues()
 | 
| +  except auth.AuthenticationError as e:
 | 
| +    print "auth.AuthenticationError: %s" % e
 | 
|  
 | 
|    print '\n\n\n'
 | 
|  
 | 
| 
 |