diff --git a/i3pystatus/github.py b/i3pystatus/github.py index 3e08f6d..65d380a 100644 --- a/i3pystatus/github.py +++ b/i3pystatus/github.py @@ -122,12 +122,14 @@ class Github(IntervalModule): The below example enables desktop notifications, enables Pango hinting for differently-colored **update_error** and **refresh_icon** text, and alters the both the status text and the colors used to visually denote the current - status level. + status level. It also sets the log level to debug, for troubleshooting + purposes. .. code-block:: python status.register( 'github', + log_level=logging.DEBUG, notify_status=True, notify_unread=True, access_token='0123456789abcdef0123456789abcdef01234567', @@ -146,6 +148,11 @@ class Github(IntervalModule): }, ) + .. note:: + Setting debug logging and authenticating with an access token will + include the access token in the log file, as the notification URL is + logged at this level. + .. _`GitHub Status API`: https://status.github.com/api .. _`GitHub Status Dashboard`: https://status.github.com .. _`notifications page`: https://github.com/notifications @@ -465,7 +472,7 @@ class Github(IntervalModule): # Auth not configured self.logger.debug( 'No auth configured, notifications will not be checked') - return False + return True if not HAS_REQUESTS: self.logger.error( @@ -473,41 +480,114 @@ class Github(IntervalModule): self.failed_update = True return False - try: - self.logger.debug( - 'Making API request to retrieve unread notifications') - if self.access_token: - self.logger.debug('Authenticating using access_token') - response = requests.get( - ACCESS_TOKEN_AUTH_URL % self.access_token) - else: - self.logger.debug('Authenticating using username/password') - response = requests.get(BASIC_AUTH_URL, - auth=(self.username, self.password)) - self.logger.log(5, - 'Raw return from GitHub notification check: %s', - response.text) - unread_data = json.loads(response.text) - except (requests.ConnectionError, requests.Timeout) as exc: - self.logger.error('Failed to check unread notifications: %s', exc) - self.failed_update = True - return False - except json.decoder.JSONDecodeError as exc: - self.logger.error('Error loading JSON: %s', exc) - self.logger.debug('JSON text that failed to load: %s', response.text) - self.failed_update = True - return False + self.logger.debug( + 'Checking unread notifications using %s', + 'access token' if self.access_token else 'username/password' + ) - # Bad credentials or some other error - if isinstance(unread_data, dict): - raise ConfigError( - unread_data.get( - 'message', - 'Unknown error encountered retrieving unread notifications' + old_unread_url = None + if self.access_token: + unread_url = ACCESS_TOKEN_AUTH_URL % self.access_token + else: + unread_url = BASIC_AUTH_URL + + self.current_unread = set() + page_num = 0 + while old_unread_url != unread_url: + old_unread_url = unread_url + page_num += 1 + self.logger.debug( + 'Reading page %d of notifications (%s)', + page_num, unread_url + ) + try: + if self.access_token: + response = requests.get(unread_url) + else: + response = requests.get( + unread_url, + auth=(self.username, self.password) + ) + self.logger.log( + 5, + 'Raw return from GitHub notification check: %s', + response.text) + unread_data = json.loads(response.text) + except (requests.ConnectionError, requests.Timeout) as exc: + self.logger.error( + 'Failed to check unread notifications: %s', exc) + self.failed_update = True + return False + except json.decoder.JSONDecodeError as exc: + self.logger.error('Error loading JSON: %s', exc) + self.logger.debug( + 'JSON text that failed to load: %s', response.text) + self.failed_update = True + return False + + # Bad credentials or some other error + if isinstance(unread_data, dict): + raise ConfigError( + unread_data.get( + 'message', + 'Unknown error encountered retrieving unread notifications' + ) ) + + # Update the current count of unread notifications + self.current_unread.update( + [x['id'] for x in unread_data if 'id' in x] ) - self.current_unread = set([x['id'] for x in unread_data if 'id' in x]) + # Check 'Link' header for next page of notifications + # (https://tools.ietf.org/html/rfc5988#section-5) + self.logger.debug('Checking for next page of notifications') + try: + link_header = response.headers['Link'] + except AttributeError: + self.logger.error( + 'No headers present in response. This might be due to ' + 'an API change in the requests module.' + ) + self.failed_update = True + continue + except KeyError: + self.logger.debug('Only one page of notifications present') + continue + else: + # Process 'Link' header + try: + links = requests.utils.parse_header_links(link_header) + except Exception as exc: + self.logger.error( + 'Failed to parse \'Link\' header: %s', exc + ) + self.failed_update = True + continue + + for link in links: + try: + link_rel = link['rel'] + if link_rel != 'next': + # Link does not refer to the next page, skip it + continue + # Set the unread_url so that when we reach the top + # of the outer loop, we have a new URL to check. + unread_url = link['url'] + break + except TypeError: + # Malformed hypermedia link + self.logger.warning( + 'Malformed hypermedia link (%s) in \'Link\' ' + 'header (%s)', link, links + ) + continue + else: + self.logger.debug('No more pages of notifications remain') + + if self.failed_update: + return False + self.data['unread_count'] = len(self.current_unread) self.data['unread'] = self.unread_marker \ if self.data['unread_count'] > 0 \